Skip to content

Test Case Design Techniques — Equivalence Partitioning & Boundary Value Analysis

DodaTech Updated 2026-06-24 9 min read

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

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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