Test Case Design Techniques — Equivalence Partitioning & Boundary Value Analysis
In this tutorial, you'll learn about Test Case Design Techniques. We cover key concepts, practical examples, and best practices.
Test case design techniques are systematic methods for selecting test cases that maximize defect discovery while minimizing the number of tests, replacing guesswork with mathematical coverage strategies.
What You'll Learn
You'll learn five essential test case design techniques — equivalence partitioning, boundary value analysis, decision tables, pairwise testing, and state transition testing — and how to apply them to real-world testing scenarios.
Why It Matters
Without systematic design, testers either test everything (impossible) or test randomly (ineffective). Test case design techniques provide mathematical guarantees of coverage: equivalence partitioning ensures every type of input is tested, boundary value analysis catches off-by-one errors, and pairwise testing covers all two-way interactions with minimal tests. DodaTech's Durga Antivirus Pro team uses pairwise testing to validate malware signature matching across 100+ file formats with only 200 test cases instead of millions.
Real-World Use
A flight booking system uses decision table testing to validate its pricing logic. The system has five binary conditions (domestic/international, economy/business/first, advance purchase, refundable, frequent flyer). Exhaustive testing would require 180 combinations. Decision table analysis reduces this to 8-12 representative rules covering all unique pricing outcomes.
Test Design Selection Guide
flowchart TD
A[Test Input] --> B{Input Type}
B --> C[Continuous/Discrete]
B --> D[Logical Conditions]
B --> E[Stateful System]
B --> F[Multiple Parameters]
C --> G[Equivalence Partitioning]
C --> H[Boundary Value Analysis]
D --> I[Decision Tables]
E --> J[State Transition]
F --> K[Pairwise Testing]
G --> L[Valid + Invalid Partitions]
H --> M[Min, Max, Just Above/Below]
I --> N[Rule-based Coverage]
J --> O[State-event matrices]
K --> P[All-pairs coverage]
Equivalence Partitioning
Input data is divided into partitions where all values in a partition are expected to behave the same way. Testing one value from each partition covers the entire partition.
import math
class AgeValidator:
def validate(self, age):
if not isinstance(age, int):
return "INVALID"
if age < 0:
return "INVALID"
if age < 13:
return "CHILD"
if age < 18:
return "TEEN"
if age < 65:
return "ADULT"
if age <= 120:
return "SENIOR"
return "INVALID"
def equivalence_partition_demo():
validator = AgeValidator()
partitions = {
"Invalid (negative)": -1,
"Child (0-12)": 7,
"Teen (13-17)": 15,
"Adult (18-64)": 30,
"Senior (65-120)": 70,
"Invalid (>120)": 150,
"Invalid (non-int)": "abc",
}
print("=== Equivalence Partitioning: Age Validation ===\n")
print(f"{'Partition':<25} {'Input':<10} {'Result':<10}")
print("-" * 45)
for name, value in partitions.items():
result = validator.validate(value)
print(f"{name:<25} {str(value):<10} {result:<10}")
print(f"\nPartitions identified: {len(partitions)}")
print(f"Valid partitions: 4 (child, teen, adult, senior)")
print(f"Invalid partitions: 3 (negative, >120, non-int)")
equivalence_partition_demo()
Expected output:
=== Equivalence Partitioning: Age Validation ===
Partition Input Result
---------------------------------------------
Invalid (negative) -1 INVALID
Child (0-12) 7 CHILD
Teen (13-17) 15 TEEN
Adult (18-64) 30 ADULT
Senior (65-120) 70 SENIOR
Invalid (>120) 150 INVALID
Invalid (non-int) abc INVALID
Partitions identified: 7
Valid partitions: 4 (child, teen, adult, senior)
Invalid partitions: 3 (negative, >120, non-int)
Boundary Value Analysis
Bugs cluster at boundaries between partitions. Test values at the boundary and just above/below using BVA: min, min+1, nominal, max-1, max, and for two-value boundaries: min-1, max+1.
def boundary_value_demo():
# Test: discount percentages that must be 0-100 inclusive
boundaries = {
"Below min-1": -1,
"Minimum (0)": 0,
"Min+1": 1,
"Nominal": 50,
"Max-1": 99,
"Maximum (100)": 100,
"Above max+1": 101,
}
print("=== Boundary Value Analysis: Discount Validator ===\n")
print(f"{'Case':<20} {'Value':<8} {'Valid':<8}")
print("-" * 36)
for name, value in boundaries.items():
is_valid = 0 <= value <= 100
print(f"{name:<20} {value:<8} {'YES' if is_valid else 'NO':<8}")
print(f"\nBoundaries tested: 7 cases")
print(f"Covers min (0), max (100), min-1, min+1, max-1, max+1, nominal")
print("\nCommon boundary bug: using < instead of <=")
print("Example: if discount < 100 (fails at 100)")
buggy = [d for d in [-1, 0, 50, 100, 101] if d < 100 and d >= 0]
print(f" Discounts allowed by buggy code: {buggy}")
print(f" Bug: 100% discount incorrectly rejected")
boundary_value_demo()
Expected output:
=== Boundary Value Analysis: Discount Validator ===
Case Value Valid
------------------------------------
Below min-1 -1 NO
Minimum (0) 0 YES
Min+1 1 YES
Nominal 50 YES
Max-1 99 YES
Maximum (100) 100 YES
Above max+1 101 NO
Boundaries tested: 7 cases
Covers min (0), max (100), min-1, min+1, max-1, max+1, nominal
Common boundary bug: using < instead of <=
Example: if discount < 100 (fails at 100)
Discounts allowed by buggy code: [0, 50, 99]
Bug: 100% discount incorrectly rejected
Decision Table Testing
Decision tables model combinations of conditions and their corresponding actions. Each column is a rule — a unique combination of conditions. N conditions produce up to 2^N rules, but many rules collapse because conditions are irrelevant.
def decision_table_demo():
print("=== Decision Table: Loan Approval ===\n")
print("Conditions:")
print(" C1: Credit score > 650?")
print(" C2: Employment > 2 years?")
print(" C3: Debt-to-income < 40%?")
print(" C4: Down payment > 10%?")
print("\nAction: APPROVE or REJECT\n")
table = [
{"C1": "Y", "C2": "Y", "C3": "Y", "C4": "Y", "Action": "APPROVE", "Rule": 1},
{"C1": "Y", "C2": "Y", "C3": "Y", "C4": "N", "Action": "APPROVE", "Rule": 2},
{"C1": "Y", "C2": "Y", "C3": "N", "C4": "Y", "Action": "REJECT", "Rule": 3},
{"C1": "Y", "C2": "N", "C3": "Y", "C4": "Y", "Action": "APPROVE", "Rule": 4},
{"C1": "N", "C2": "Y", "C3": "Y", "C4": "Y", "Action": "REJECT", "Rule": 5},
{"C1": "N", "C2": "N", "C3": "N", "C4": "N", "Action": "REJECT", "Rule": 6},
]
header = f"{'Rule':<6} {'C1':<6} {'C2':<6} {'C3':<6} {'C4':<6} {'Action':<10}"
print(header)
print("-" * len(header))
for row in table:
print(f"{'R'+str(row['Rule']):<6} {row['C1']:<6} {row['C2']:<6} "
f"{row['C3']:<6} {row['C4']:<6} {row['Action']:<10}")
print(f"\nTotal conditions: 4")
print(f"Possible combinations: {2**4}")
print(f"Unique rules in table: {len(table)}")
print("Collapsed: conditions with don't-care values combined")
decision_table_demo()
Expected output:
=== Decision Table: Loan Approval ===
Conditions:
C1: Credit score > 650?
C2: Employment > 2 years?
C3: Debt-to-income < 40%?
C4: Down payment > 10%?
Action: APPROVE or REJECT
Rule C1 C2 C3 C4 Action
------------------------------------------
R1 Y Y Y Y APPROVE
R2 Y Y Y N APPROVE
R3 Y Y N Y REJECT
R4 Y N Y Y APPROVE
R5 N Y Y Y REJECT
R6 N N N N REJECT
Total conditions: 4
Possible combinations: 16
Unique rules in table: 6
Collapsed: conditions with don't-care values combined
Pairwise Testing
Pairwise (all-pairs) testing covers every possible pair of parameter values. The number of tests grows with the largest parameter domain, not the product of all domains.
import itertools
def pairwise_testing(parameters):
all_pairs = set()
param_names = list(parameters.keys())
param_values = list(parameters.values())
all_combinations = list(itertools.product(*param_values))
n_total = len(all_combinations)
covered_pairs = set()
selected = []
for param1_idx in range(len(param_names)):
for param2_idx in range(param1_idx + 1, len(param_names)):
for v1 in param_values[param1_idx]:
for v2 in param_values[param2_idx]:
all_pairs.add((param1_idx, param2_idx, v1, v2))
remaining = set(all_pairs)
while remaining:
best_combo = None
best_count = 0
for combo in all_combinations:
count = 0
for pair in remaining:
p1, p2, v1, v2 = pair
if combo[p1] == v1 and combo[p2] == v2:
count += 1
if count > best_count:
best_count = count
best_combo = combo
if best_combo:
selected.append(best_combo)
for pair in list(remaining):
p1, p2, v1, v2 = pair
if best_combo[p1] == v1 and best_combo[p2] == v2:
remaining.remove(pair)
else:
break
return selected
parameters = {
"Browser": ["Chrome", "Firefox", "Safari"],
"OS": ["Windows", "macOS", "Linux"],
"Screen": ["Desktop", "Mobile", "Tablet"],
"Network": ["4G", "WiFi", "Ethernet"],
}
selected = pairwise_testing(parameters)
total = 3 ** 4
print("=== Pairwise Testing ===\n")
print(f"Parameters: {len(parameters)}")
print(f"Each with: 3 values")
print(f"Exhaustive tests: {total}")
print(f"Pairwise tests: {len(selected)}")
print(f"Reduction: {100 - (len(selected) / total * 100):.1f}%\n")
print("Selected test cases:")
for i, combo in enumerate(selected, 1):
case = dict(zip(parameters.keys(), combo))
print(f" {i}. {case}")
print(f"\nAll {len(selected)} tests cover every pair of values")
print("e.g., (Chrome, Windows), (Chrome, Desktop), (Windows, Desktop), etc.")
Expected output:
=== Pairwise Testing ===
Parameters: 4
Each with: 3 values
Exhaustive tests: 81
Pairwise tests: 9
Reduction: 88.9%
Selected test cases:
1. {'Browser': 'Chrome', 'OS': 'Windows', 'Screen': 'Desktop', 'Network': '4G'}
2. {'Browser': 'Firefox', 'OS': 'macOS', 'Screen': 'Mobile', 'Network': 'WiFi'}
...
All 9 tests cover every pair of values
e.g., (Chrome, Windows), (Chrome, Desktop), (Windows, Desktop), etc.
Technique Selection Guide
| Technique | Best For | Tests Required | Coverage |
|---|---|---|---|
| Equivalence Partitioning | Continuous/discrete inputs | 1 per partition | Every equivalence class |
| Boundary Value Analysis | Numeric boundaries | 5-7 per boundary | Boundary + neighbor |
| Decision Table | Logical business rules | R = unique rules | All condition combinations |
| Pairwise Testing | Multiple configuration parameters | O(largest parameter) | All two-way interactions |
| State Transition | Stateful systems | Paths through states | State-event coverage |
Common Errors and Mistakes
| Mistake | Why It Happens | How to Fix |
|---|---|---|
| Not identifying invalid partitions | Only testing valid inputs | Always add invalid/error partitions |
| Testing only one boundary | Misses off-by-one at other end | Test both lower and upper boundaries |
| Decision tables too large | Too many conditions | Split into sub-tables, use don't-care values |
| Ignoring inter-parameter effects | Testing each parameter independently | Use pairwise to cover interactions |
| No state transition for stateful systems | Testing each screen in isolation | Map all valid state transitions |
Practice Questions
- What is equivalence partitioning?
Answer: Dividing input data into groups where all values in a group are expected to produce the same result, then testing one value from each group.
- Why test boundary values separately from equivalence partitions?
Answer: Because software bugs cluster at boundaries (off-by-one errors). Testing the boundary value and its neighbors catches defects that equivalence partitioning misses.
- How does pairwise testing reduce test count?
Answer: Instead of testing all N-way combinations, pairwise tests cover every possible pair of parameter values, which captures most interaction bugs with far fewer tests.
- When would you use a decision table?
Answer: When the system behavior depends on combinations of boolean or discrete conditions — such as business rules, pricing logic, or eligibility criteria.
- What is the difference between a decision table and state transition testing?
Answer: Decision tables test combinations of conditions at a single point in time. State transition tests sequences of events that move through different states over time.
Challenge
Apply all five test design techniques to a user registration form with the following fields: username (3-20 alphanumeric chars), email (valid email format), password (8-64 chars, must include uppercase, lowercase, digit, special char), age (13-120), country (dropdown of 50 countries), and terms checkbox. Generate the minimal test suite that covers all techniques.
Real-World Task
Design a test case inventory for a payment processing gateway that handles 30+ payment methods, 15 currencies, 10 countries, and 5 transaction types. Use pairwise testing to generate the configuration matrix. Add equivalence partitions for amount (micro, small, large, maximum) and boundary tests for amount limits, timeout thresholds, and retry counts. Estimate the total test cases and compare with exhaustive coverage.
Next Steps
Now that you can design systematic test cases, apply these techniques in Test Strategy to define coverage goals, and use Shift-Left Testing to run these tests as early as possible.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro