White box (as discussed in Grey Box Testing: Best of Both Worlds) testing is a testing approach where testers examine the internal structure, design, and implementation of code. Unlike black box testing that focuses on inputs and outputs, white box testing validates the internal logic, code paths, and algorithms. It’s also called structural testing, clear box testing, or glass box testing.
What is White Box Testing?
White box testing involves analyzing source code to design test cases that exercise specific code paths, conditions, and logic. Testers need programming knowledge and access to source code to perform white box testing effectively.
Key Characteristics
- Code visibility: Full access to source code and internal structure
- Structural focus: Tests internal logic, algorithms, and code paths
- Developer-centric: Often performed by developers or technical testers
- Coverage-driven: Aims for high code coverage metrics
- Early defect detection: Finds bugs during development phase
When to Use White Box Testing
White box testing is ideal for:
- Unit testing: Testing individual functions and methods
- Integration testing: Verifying component interactions
- Security testing: Finding vulnerabilities in code logic
- Code optimization: Identifying performance bottlenecks
- Algorithm validation: Ensuring calculations are correct
Code Coverage Metrics
1. Statement Coverage
Statement coverage measures the percentage of executable statements executed by tests. Every line of code should run at least once.
Formula: Statement Coverage = (Executed Statements / Total Statements) × 100%
Example:
def calculate_discount(price, is_member, purchase_count):
"""Calculate discount based on membership and purchase history"""
discount = 0 # Statement 1
if is_member: # Statement 2
discount = 10 # Statement 3
if purchase_count > 5: # Statement 4
discount += 5 # Statement 5
return discount # Statement 6
# Test case 1: Non-member, few purchases
def test_no_discount():
result = calculate_discount(100, False, 2)
assert result == 0
# Coverage: Statements 1, 2, 4, 6 = 4/6 = 67%
# Test case 2: Member with many purchases
def test_full_discount():
result = calculate_discount(100, True, 10)
assert result == 15
# Coverage: All statements = 6/6 = 100%
Limitations: 100% statement coverage doesn’t guarantee all scenarios are tested—only that all lines executed.
2. Branch Coverage (Decision Coverage)
Branch coverage ensures every decision point (if/else, switch, loops) evaluates to both true and false. It’s stronger than statement coverage.
Formula: Branch Coverage = (Executed Branches / Total Branches) × 100%
Example:
def validate_password(password):
"""Validate password strength"""
if len(password) < 8: # Decision 1
return "Too short"
if not any(c.isupper() for c in password): # Decision 2
return "Needs uppercase"
if not any(c.isdigit() for c in password): # Decision 3
return "Needs digit"
return "Valid"
# Branches:
# Decision 1: True, False
# Decision 2: True, False
# Decision 3: True, False
# Total: 6 branches
def test_branch_coverage():
# Test 1: Covers D1=True
assert validate_password("short") == "Too short"
# Test 2: Covers D1=False, D2=True
assert validate_password("lowercase123") == "Needs uppercase"
# Test 3: Covers D1=False, D2=False, D3=True
assert validate_password("NoDigits") == "Needs digit"
# Test 4: Covers D1=False, D2=False, D3=False
assert validate_password("Valid123") == "Valid"
# Branch Coverage: 6/6 = 100%
3. Path Coverage
Path coverage tests all possible paths through the code. For code with multiple decisions, the number of paths grows exponentially.
Example:
def process_order(item_count, is_premium, in_stock):
"""Process order with multiple conditions"""
message = ""
if item_count > 0: # Decision A
if in_stock: # Decision B
message = "Processing"
if is_premium: # Decision C
message += " with priority"
else:
message = "Out of stock"
else:
message = "Invalid count"
return message
# Possible paths:
# Path 1: A=False → "Invalid count"
# Path 2: A=True, B=False → "Out of stock"
# Path 3: A=True, B=True, C=False → "Processing"
# Path 4: A=True, B=True, C=True → "Processing with priority"
def test_all_paths():
# Path 1
assert process_order(0, False, True) == "Invalid count"
# Path 2
assert process_order(5, False, False) == "Out of stock"
# Path 3
assert process_order(5, False, True) == "Processing"
# Path 4
assert process_order(5, True, True) == "Processing with priority"
# Path Coverage: 4/4 = 100%
Challenge: For n independent decisions, there are 2^n possible paths. Path coverage becomes impractical for complex code.
4. Condition Coverage
Condition coverage ensures each boolean sub-expression evaluates to both true and false independently.
Example:
def can_access(is_authenticated, has_permission, is_active):
"""Check if user can access resource"""
# Compound condition with 3 boolean sub-expressions
if is_authenticated and has_permission and is_active:
return "Access granted"
return "Access denied"
# For 100% condition coverage, each variable must be True and False:
def test_condition_coverage():
# is_authenticated: True
can_access(True, True, True)
# is_authenticated: False
can_access(False, True, True)
# has_permission: True (already covered above)
# has_permission: False
can_access(True, False, True)
# is_active: True (already covered)
# is_active: False
can_access(True, True, False)
# Condition Coverage: 100%
5. Multiple Condition Coverage (MCC)
Multiple condition coverage tests all combinations of boolean sub-expressions.
Example:
def authorize_action(is_admin, is_owner):
"""Authorize action based on role and ownership"""
if is_admin or is_owner:
return True
return False
# Truth table for 'is_admin or is_owner':
# is_admin | is_owner | Result
# T | T | T
# T | F | T
# F | T | T
# F | F | F
def test_mcc():
assert authorize_action(True, True) == True # T, T
assert authorize_action(True, False) == True # T, F
assert authorize_action(False, True) == True # F, T
assert authorize_action(False, False) == False # F, F
# MCC: 4/4 = 100%
White Box Testing Techniques
1. Control Flow Testing
Control flow testing visualizes code as a graph where nodes are statements and edges are control flow paths.
Example: Flow graph for login validation
def validate_login(username, password):
# Node 1: Entry
if not username: # Node 2
return "Username required" # Node 3
if len(password) < 8: # Node 4
return "Password too short" # Node 5
user = find_user(username) # Node 6
if user and check_password(user, password): # Node 7
return "Success" # Node 8
return "Invalid credentials" # Node 9
# Flow graph:
# 1
# ↓
# 2 → 3 (return)
# ↓
# 4 → 5 (return)
# ↓
# 6
# ↓
# 7 → 8 (return)
# ↓
# 9 (return)
# Cyclomatic Complexity = E - N + 2P
# E=edges, N=nodes, P=connected components
# Complexity = 3 (3 decision points + 1)
2. Data Flow Testing
Data flow testing tracks variable definitions and uses to ensure correct data handling.
Example:
def calculate_total(items):
"""Calculate total price with discount logic"""
total = 0 # Definition of 'total'
for item in items:
total += item.price # Use of 'total', then re-definition
discount = 0 # Definition of 'discount'
if total > 100: # Use of 'total'
discount = total * 0.1 # Re-definition of 'discount'
final = total - discount # Use of 'total' and 'discount'
return final
# Data flow anomalies to test:
# - Defined but never used (dd)
# - Used but never defined (ur)
# - Defined twice without use between (dd)
def test_data_flow():
# Test variable lifecycle
items = [Item(50), Item(60)] # total=110, discount=11
assert calculate_total(items) == 99
items = [Item(30), Item(40)] # total=70, discount=0
assert calculate_total(items) == 70
3. Loop Testing
Loop testing validates loop behavior at boundaries and during execution.
Loop testing strategies:
def process_batch(items, max_retries=3):
"""Process items with retry logic"""
processed = []
for item in items: # Loop 1
retry_count = 0
while retry_count < max_retries: # Loop 2 (nested)
if process_item(item):
processed.append(item)
break
retry_count += 1
else:
# Executed if loop completes without break
log_failure(item)
return processed
def test_loops():
# Simple loop tests:
# 1. Skip loop (0 iterations)
assert len(process_batch([])) == 0
# 2. Single iteration
assert len(process_batch([Item(1)])) == 1
# 3. Two iterations
assert len(process_batch([Item(1), Item(2)])) == 2
# 4. Maximum iterations
items = [Item(i) for i in range(100)]
assert len(process_batch(items)) <= 100
# Nested loop tests:
# 5. Inner loop executes max times
failing_item = FailingItem()
result = process_batch([failing_item])
# Verify retry_count reached max_retries
4. Mutation Testing
Mutation testing introduces small code changes (mutations) to verify tests can detect them.
Example:
# Original code
def is_eligible(age, has_license):
return age >= 18 and has_license
# Mutation 1: Change >= to >
def is_eligible_mutant1(age, has_license):
return age > 18 and has_license # Mutant
# Mutation 2: Change 'and' to 'or'
def is_eligible_mutant2(age, has_license):
return age >= 18 or has_license # Mutant
# Tests must kill mutations
def test_eligibility():
# This test kills Mutation 1 (age=18 should pass but fails in mutant)
assert is_eligible(18, True) == True
# This test kills Mutation 2 (should fail without license)
assert is_eligible(20, False) == False
# Mutation Score = Killed Mutations / Total Mutations
# Score = 2/2 = 100%
White Box Testing Tools
Static Analysis Tools
# Example: Using pylint for static analysis
# Run: pylint my_module.py
def calculate_interest(principal, rate, time):
"""Calculate simple interest"""
result = principal * rate * time / 100
return result
# Pylint checks:
# - Unused variables
# - Undefined variables
# - Type mismatches
# - Code smells
# - Complexity metrics
Code Coverage Tools
# Using pytest-cov for coverage reporting
# Run: pytest --cov=myapp --cov-report=html
# coverage report example:
"""
Name Stmts Miss Cover
-------------------------------------------
myapp/auth.py 45 2 96%
myapp/orders.py 67 8 88%
myapp/payment.py 34 0 100%
-------------------------------------------
TOTAL 146 10 93%
"""
# .coveragerc configuration
"""
[run]
source = myapp
omit = */tests/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
"""
Profiling Tools
import cProfile
import pstats
def analyze_performance():
"""Profile code execution"""
profiler = cProfile.Profile()
profiler.enable()
# Code to profile
result = expensive_operation()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats()
# Output shows:
# - Function calls
# - Time per function
# - Call counts
# - Hotspots for optimization
White Box Testing Best Practices
1. Aim for High Coverage, Not 100%
# Focus on critical paths over complete coverage
class PaymentProcessor:
def process_payment(self, amount, method):
"""Critical path - must have 100% coverage"""
if amount <= 0:
raise ValueError("Invalid amount")
if method == "credit_card":
return self._process_credit_card(amount)
elif method == "paypal":
return self._process_paypal(amount)
else:
raise ValueError("Unknown method")
def format_receipt(self, transaction):
"""Low risk - 80% coverage acceptable"""
# Less critical formatting logic
pass
# Prioritize coverage for:
# - Payment processing: 100%
# - Security functions: 100%
# - Business logic: 95%+
# - UI formatting: 70-80%
2. Test Edge Cases and Boundaries
def get_age_category(age):
"""Categorize age groups"""
if age < 0:
raise ValueError("Age cannot be negative")
elif age < 13:
return "child"
elif age < 20:
return "teenager"
elif age < 65:
return "adult"
else:
return "senior"
def test_edge_cases():
# Invalid input
with pytest.raises(ValueError):
get_age_category(-1)
# Boundaries
assert get_age_category(0) == "child"
assert get_age_category(12) == "child"
assert get_age_category(13) == "teenager"
assert get_age_category(19) == "teenager"
assert get_age_category(20) == "adult"
assert get_age_category(64) == "adult"
assert get_age_category(65) == "senior"
assert get_age_category(100) == "senior"
3. Use Mocking for Dependencies
from unittest.mock import Mock, patch
class UserService:
def __init__(self, db, email_service):
self.db = db
self.email_service = email_service
def register_user(self, email, password):
"""Register new user"""
if self.db.user_exists(email):
return False
user = self.db.create_user(email, password)
self.email_service.send_welcome(user)
return True
def test_registration_with_mocks():
# Mock dependencies
mock_db = Mock()
mock_email = Mock()
# Configure mock behavior
mock_db.user_exists.return_value = False
mock_db.create_user.return_value = {"id": 1, "email": "test@example.com"}
# Test
service = UserService(mock_db, mock_email)
result = service.register_user("test@example.com", "password123")
# Verify
assert result == True
mock_db.user_exists.assert_called_once_with("test@example.com")
mock_db.create_user.assert_called_once()
mock_email.send_welcome.assert_called_once()
4. Test Error Handling
def divide_numbers(a, b):
"""Divide two numbers with error handling"""
try:
result = a / b
return {"success": True, "result": result}
except ZeroDivisionError:
return {"success": False, "error": "Division by zero"}
except TypeError:
return {"success": False, "error": "Invalid types"}
def test_error_handling():
# Happy path
result = divide_numbers(10, 2)
assert result["success"] == True
assert result["result"] == 5
# ZeroDivisionError path
result = divide_numbers(10, 0)
assert result["success"] == False
assert "zero" in result["error"]
# TypeError path
result = divide_numbers("10", "2")
assert result["success"] == False
assert "types" in result["error"]
Real-World White Box Testing Example
Testing a Shopping Cart System
class ShoppingCart:
def __init__(self):
self.items = []
self.discount_code = None
def add_item(self, product, quantity):
"""Add item to cart"""
if quantity <= 0:
raise ValueError("Quantity must be positive")
# Check if item already exists
for item in self.items:
if item['product'].id == product.id:
item['quantity'] += quantity
return
self.items.append({
'product': product,
'quantity': quantity
})
def calculate_total(self):
"""Calculate cart total with discounts"""
subtotal = sum(
item['product'].price * item['quantity']
for item in self.items
)
discount = 0
if self.discount_code:
if self.discount_code.is_valid():
if self.discount_code.type == "percentage":
discount = subtotal * (self.discount_code.value / 100)
elif self.discount_code.type == "fixed":
discount = min(self.discount_code.value, subtotal)
return max(subtotal - discount, 0)
# Comprehensive white box tests
class TestShoppingCart:
def test_add_item_new_product(self):
"""Test adding new product (path: item not in cart)"""
cart = ShoppingCart()
product = Product(id=1, price=10)
cart.add_item(product, 2)
assert len(cart.items) == 1
assert cart.items[0]['quantity'] == 2
def test_add_item_existing_product(self):
"""Test adding existing product (path: item in cart)"""
cart = ShoppingCart()
product = Product(id=1, price=10)
cart.add_item(product, 2)
cart.add_item(product, 3)
assert len(cart.items) == 1
assert cart.items[0]['quantity'] == 5
def test_add_item_invalid_quantity(self):
"""Test error handling for invalid quantity"""
cart = ShoppingCart()
product = Product(id=1, price=10)
with pytest.raises(ValueError):
cart.add_item(product, 0)
with pytest.raises(ValueError):
cart.add_item(product, -1)
def test_calculate_total_no_discount(self):
"""Test total calculation without discount"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=10), 2)
cart.add_item(Product(id=2, price=15), 1)
assert cart.calculate_total() == 35
def test_calculate_total_percentage_discount(self):
"""Test percentage discount path"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="percentage", value=10, valid=True)
assert cart.calculate_total() == 90
def test_calculate_total_fixed_discount(self):
"""Test fixed discount path"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="fixed", value=20, valid=True)
assert cart.calculate_total() == 80
def test_calculate_total_fixed_discount_exceeds_subtotal(self):
"""Test fixed discount doesn't go negative"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=10), 1)
cart.discount_code = DiscountCode(type="fixed", value=50, valid=True)
assert cart.calculate_total() == 0
def test_calculate_total_invalid_discount_code(self):
"""Test invalid discount code path"""
cart = ShoppingCart()
cart.add_item(Product(id=1, price=100), 1)
cart.discount_code = DiscountCode(type="percentage", value=10, valid=False)
assert cart.calculate_total() == 100
Coverage Report Analysis
# Run coverage
$ pytest --cov=shopping_cart --cov-report=term-missing
Name Stmts Miss Branch BrPart Cover Missing
---------------------------------------------------------------
shopping_cart.py 45 0 18 0 100%
---------------------------------------------------------------
TOTAL 45 0 18 0 100%
# Branch coverage details:
# - add_item: 4/4 branches (100%)
# - calculate_total: 14/14 branches (100%)
Advantages and Limitations
Advantages
- Early bug detection: Finds defects during development
- Code optimization: Identifies inefficiencies and dead code
- Complete coverage: Can test all code paths systematically
- Algorithm validation: Verifies complex calculations
- Security: Reveals vulnerabilities in logic
Limitations
- Requires code access: Not suitable for third-party systems
- Programming knowledge: Testers need technical skills
- Time-consuming: Thorough testing takes significant effort
- Maintenance overhead: Tests break when code changes
- Doesn’t validate requirements: May miss functional gaps
Conclusion
White box testing provides deep insight into code quality by examining internal structures and logic. Through techniques like statement coverage, branch coverage, path coverage, and data flow analysis, you can systematically verify code behaves correctly under all conditions.
The key to effective white box testing is balancing coverage goals with practical constraints. Aim for high coverage on critical paths, use appropriate tools to measure and track coverage, and combine white box techniques with black box testing for comprehensive quality assurance.
Whether you’re writing unit tests for individual functions or analyzing complex algorithms, white box testing ensures your code is robust, efficient, and maintainable.