White Box Testing
White box testing, also known as structural or glass-box testing, is a method where the tester has full visibility into the internal workings of the system. The tester examines the code, architecture, and logic paths to verify that the system behaves as expected and is free of bugs. This method contrasts with black box testing, where the internal structure is not known to the tester. White box testing is typically performed by developers or testers who understand the codebase, allowing them to identify and fix issues that may not be apparent through black box testing alone.
Types of White Box Testing:
Unit Testing: Testing individual components or functions of the code. Unit tests verify that each part of the code works as expected in isolation.
Code Coverage Testing: Ensures that all parts of the code are executed at least once during testing, which includes path coverage, statement coverage, branch coverage, and condition coverage.
Control Flow Testing: Focuses on the execution order of instructions in the code, ensuring that all possible paths are tested.
Data Flow Testing: Ensures that data variables are used correctly throughout the code and that no variables are initialized but never used, or used before being initialized.
Mutation Testing: Introduces small changes to the code to check if the existing test cases can detect the modified (or "mutated") code.
Advantages of White Box Testing:
High Accuracy: Since testers have full access to the code, they can pinpoint the exact location of a bug.
Optimization: It helps in optimizing the code by removing redundant or inefficient code.
Early Bug Detection: As unit testing is part of white box testing, bugs can be detected and fixed early in the development process.
Challenges of White Box Testing:
Time-Consuming: Thorough white box testing, especially with complex applications, can take considerable time.
Requires Programming Knowledge: Testers need to understand the code to be effective, which requires a higher skill set than black box testing.
White Box Testing Example: Payment Processing System
Imagine we are working with an online e-commerce platform that handles payment transactions. The payment processing system is critical, and any bugs could result in lost revenue or data breaches. White box testing is ideal here because we need to ensure that the internal logic, code structure, and data flow are correct and secure.
Step 1: Review the Code
The first step in white box testing is gaining access to the source code. In this case, we have a payment processing module that includes functions for
Validating payment details (credit card number, expiration date, CVV).
Processing the transaction by communicating with a payment gateway.
Handling transaction responses (approved, declined, or error).
Here’s a simplified version of the code for validating credit card details:
def validate_credit_card(card_number, expiration_date, cvv):
if len(card_number) != 16 or not card_number.isdigit():
return "Invalid Card Number"
if expiration_date < current_date():
return "Expired Card"
if len(cvv) != 3 or not cvv.isdigit():
return "Invalid CVV"
return "Card is Valid"
Step 2: Identifying Key Paths and Test Cases
In white box testing, the goal is to cover all possible execution paths, conditions, and loops. For the validate_credit_card function, we need to test:
Valid Input Path: Testing a valid 16-digit card number, future expiration date, and valid 3-digit CVV.
Invalid Card Number Path: Testing card numbers that are not 16 digits long or contain non-digit characters.
Expired Card Path: Testing with an expiration date in the past.
Invalid CVV Path: Testing CVVs that are not 3 digits long or contain non-digit characters.
Step 3: Writing Test Cases
Now, we create test cases for each path:
Valid Input Test Case:
Card Number: "1234567812345678"
Expiration Date: "2025-12"
CVV: "123"
Expected Result: "Card is Valid"
Invalid Card Number Test Case (Less than 16 digits):
Card Number: "12345678"
Expiration Date: "2025-12"
CVV: "123"
Expected Result: "Invalid Card Number"
Invalid Card Number Test Case (Contains letters):
Card Number: "1234abcd1234abcd"
Expiration Date: "2025-12"
CVV: "123"
Expected Result: "Invalid Card Number"
Expired Card Test Case:
Card Number: "1234567812345678"
Expiration Date: "2020-01"
CVV: "123"
Expected Result: "Expired Card"
Invalid CVV Test Case (Less than 3 digits):
Card Number: "1234567812345678"
Expiration Date: "2025-12"
CVV: "12"
Expected Result: "Invalid CVV"
Invalid CVV Test Case (Contains letters):
Card Number: "1234567812345678"
Expiration Date: "2025-12"
CVV: "12a"
Expected Result: "Invalid CVV"
Step 4: Code Coverage Analysis:
Once the test cases are written, the next step is to analyze code coverage to ensure that all paths are exercised. In this case, the test cases from Step 3 cover:
Statement coverage: Each line of code in the validate_credit_card function is executed at least once.
Branch coverage: All possible branches (e.g., valid card number, invalid card number, expired card, valid/invalid CVV) are tested.
By using a code coverage tool, we verify that:
The valid input path, invalid card number path, expired card path, and invalid CVV path are fully covered.
We achieve condition coverage by ensuring that every condition in the if statements (e.g., len(card_number) != 16, expiration_date < current_date(), len(cvv) != 3) is tested as both true and false.
In short, the test cases from Step 3 ensure that all paths, conditions, and branches of the code are tested, achieving comprehensive code coverage.
Step 5: Control Flow Testing:
Next, we examine the control flow to ensure that the execution order of the code is correct. Using a flowchart or control flow graph, we map out the execution paths and validate that each decision point (e.g., checking the card number, expiration date, and CVV) leads to the correct outcome.
In our case:
If the card number is invalid, the function should return "Invalid Card Number" without checking the expiration date or CVV.
If the card is expired, the function should immediately return "Expired Card" without validating the CVV.
Control flow testing confirms that the function behaves correctly for all paths, avoiding unnecessary checks when a failure is detected early.
Step 6: Data Flow Testing:
We also perform data flow testing to ensure that variables are properly initialized, used, and reset within the function. For instance:
The card_number, expiration_date, and cvv variables must be initialized with input values before they are used.
We verify that these variables are only used when necessary, such as not checking the CVV if the card number is already invalid.
Step 7: Mutation Testing:
Finally, we perform mutation testing to ensure the robustness of our test cases. In this step, we introduce small changes (mutations) to the code to check if our tests can detect the errors.
For example, we might change the condition len(card_number) != 16 to len(card_number) == 16. If our test cases pass without detecting this error, it means they are not robust enough. Mutation testing helps ensure our tests can catch even subtle code changes or errors.
Step 8: Optimizing the Code:
White box testing also highlights potential areas for optimization. For example, if we notice that the function performs unnecessary checks (e.g., checking the CVV after detecting an invalid card number), we can refactor the code to make it more efficient by short-circuiting the validation process.