Combinatorial & Pairwise Testing: Maximum Coverage with Minimum Tests

Combinatorial & Pairwise Testing: Maximum Coverage with Minimum Tests

Testing every combination of input parameters is mathematically impossible for most systems. Three boolean parameters create 8 combinations. Ten parameters with 3 values each create 59,049 combinations. A car has around 500 features with binary toggles—more combinations than atoms in the universe.

Combinatorial testing solves this by using mathematics to find the minimum set of test cases that covers all n-way interactions between parameters. Pairwise testing (2-way combinatorial) covers all pairs of parameter values—and research shows that most software failures are triggered by interactions between two or fewer parameters.

The Combinatorial Testing Insight

If a bug is caused by a specific combination of parameter values, combinatorial testing guarantees you'll find it—using far fewer tests than exhaustive coverage.

Consider a configuration with 4 parameters, each with 3 values:

  • Exhaustive: 3^4 = 81 tests
  • Pairwise (2-way): 9 tests
  • 3-way: 27 tests

The pairwise set of 9 tests covers every possible pair of parameter values at least once. If any pair causes a bug, you'll catch it.

How Pairwise Testing Works

Given parameters:

Browser: Chrome, Firefox, Safari
OS: Windows, macOS, Linux
Network: Fast, Slow, Offline
Auth: Logged in, Logged out

Exhaustive testing: 3×3×3×2 = 54 tests

Pairwise testing generates a covering array—a set of test cases where every pair of (Browser, OS), (Browser, Network), (Browser, Auth), (OS, Network), (OS, Auth), and (Network, Auth) values appears at least once:

Test 1: Chrome, Windows, Fast, Logged in
Test 2: Chrome, macOS, Slow, Logged out
Test 3: Chrome, Linux, Offline, Logged in
Test 4: Firefox, Windows, Slow, Logged in
Test 5: Firefox, macOS, Offline, Logged out
Test 6: Firefox, Linux, Fast, Logged out
Test 7: Safari, Windows, Offline, Logged out
Test 8: Safari, macOS, Fast, Logged in
Test 9: Safari, Linux, Slow, Logged in

9 tests instead of 54, covering all 21 pairs.

Tools for Combinatorial Test Generation

Python: allpairspy

from allpairspy import AllPairs

parameters = [
    ["Chrome", "Firefox", "Safari"],  # Browser
    ["Windows", "macOS", "Linux"],    # OS
    ["Fast", "Slow", "Offline"],       # Network
    ["Logged in", "Logged out"],       # Auth state
]

test_cases = AllPairs(parameters)

for i, case in enumerate(test_cases):
    print(f"Test {i+1}: {case.test_parameters}")

# Output:
# Test 1: ['Chrome', 'Windows', 'Fast', 'Logged in']
# Test 2: ['Chrome', 'macOS', 'Slow', 'Logged out']
# ...

Python: Practical Test Generation

import pytest
from allpairspy import AllPairs

def generate_browser_os_tests():
    parameters = [
        ["Chrome", "Firefox", "Safari", "Edge"],
        ["Windows", "macOS", "Linux"],
        ["mobile", "desktop"],
        ["1080p", "1440p", "4K", "720p"],
    ]
    
    return [
        pytest.param(
            case.test_parameters[0],  # browser
            case.test_parameters[1],  # os
            case.test_parameters[2],  # form_factor
            case.test_parameters[3],  # resolution
            id=f"{case.test_parameters[0]}-{case.test_parameters[1]}-{case.test_parameters[2]}"
        )
        for case in AllPairs(parameters)
    ]

@pytest.mark.parametrize("browser,os,form_factor,resolution", generate_browser_os_tests())
def test_checkout_renders_correctly(browser, os, form_factor, resolution):
    """Test checkout page across all important platform combinations."""
    with Browser(browser, os=os, form_factor=form_factor, resolution=resolution) as b:
        b.goto("https://my-app.com/checkout")
        assert b.find("#checkout-form").is_visible()
        assert b.find("#submit-button").is_visible()
        # ... more assertions

Using ACTS (NIST Tool)

ACTS (Automated Combinatorial Testing for Software) is a free tool from NIST:

# acts_config.txt
[System]
Name: WebAppConfig

[Parameter]
Browser (enum) : Chrome, Firefox, Safari, Edge
OS (enum) : Windows, macOS, Linux
JavaScript (boolean) : true, false
CookiesEnabled (boolean) : true, false
CacheEnabled (boolean) : true, false
UserType (enum) : admin, regular, guest

[Constraint]
# Can't be logged in as admin if JavaScript is disabled
JavaScriptDisabled && UserType == admin => false

[Strength]
2

Run: java -jar acts.jar -t 2 acts_config.txt output.txt

This generates the minimum 2-way covering array respecting constraints.

Using pict (Microsoft Tool)

# checkout.pict
Browser:    Chrome, Firefox, Safari, Edge
OS:         Windows, macOS, Linux
AuthState:  LoggedIn, Guest
CartState:  Empty, SingleItem, MultipleItems, LargeCart
PayMethod:  CreditCard, PayPal, ApplePay, Bank
Country:    US, EU, UK, Other

# Constraints
IF [AuthState] = "Guest" THEN [SavePayment] = "No"
IF [CartState] = "Empty" THEN [PayMethod] = "N/A"

Run: pict checkout.pict /o:2

When to Use Higher-Strength Covering Arrays

Research (Kuhn et al., NIST) found:

  • 67-70% of failures triggered by single parameter value
  • 93-97% triggered by pair interactions (use pairwise)
  • 99%+ triggered by 3-way interactions (use 3-way)

Use 3-way covering arrays for high-risk, complex systems:

from allpairspy import AllPairs

# 3-way covering array
parameters = [
    ["A", "B", "C"],  # Payment method
    ["X", "Y", "Z"],  # User type
    ["1", "2", "3"],  # Currency
    ["P", "Q", "R"],  # Region
]

# For 3-way coverage, use a dedicated tool or ACTS
# allpairspy defaults to 2-way; configure strength for 3-way

Combinatorial Testing for API Testing

Combinatorial testing is excellent for API parameter testing:

from allpairspy import AllPairs
import httpx
import pytest

def get_search_api_test_cases():
    """Generate pairwise test cases for search API parameters."""
    parameters = [
        [None, "laptop", "a" * 200, "<script>alert(1)</script>"],  # query
        [None, 0, 10, 100],       # page
        [None, 10, 50, 1000],     # limit
        [None, "price", "name", "relevance"],  # sort_by
        [None, "asc", "desc"],    # sort_order
        [None, "0", "100", "999999"],  # min_price
        [None, "computers", "invalid_category", "electronics"],  # category
    ]
    
    return AllPairs(parameters)

@pytest.mark.parametrize(
    "query,page,limit,sort_by,sort_order,min_price,category",
    [(c.test_parameters) for c in get_search_api_test_cases()]
)
def test_search_api_handles_parameter_combinations(
    query, page, limit, sort_by, sort_order, min_price, category
):
    """Search API must handle all parameter combinations gracefully."""
    params = {k: v for k, v in {
        "q": query,
        "page": page,
        "limit": limit,
        "sort_by": sort_by,
        "sort_order": sort_order,
        "min_price": min_price,
        "category": category
    }.items() if v is not None}
    
    response = httpx.get("https://api.my-app.com/search", params=params)
    
    # Should never return 500 regardless of parameter combination
    assert response.status_code != 500, \
        f"Server error with params: {params}"
    
    # Valid combinations should return 200
    if query and sort_by in ["price", "name", "relevance"]:
        assert response.status_code == 200

Handling Constraints

Real systems have invalid combinations. Combinatorial test tools support constraints:

from allpairspy import AllPairs

def is_valid_combination(combination):
    """Filter out invalid combinations."""
    browser, js_enabled, auth = combination[0], combination[1], combination[2]
    
    # Can't be logged in without JavaScript
    if auth == "logged_in" and js_enabled == "false":
        return False
    
    # Safari on Linux is not a thing
    if browser == "Safari" and combination[3] == "Linux":
        return False
    
    return True

parameters = [
    ["Chrome", "Firefox", "Safari"],
    ["true", "false"],    # JS enabled
    ["logged_in", "guest"],
    ["Windows", "macOS", "Linux"]
]

test_cases = AllPairs(parameters, filter_func=is_valid_combination)

Combinatorial Testing Checklist

Before designing tests:

  • Identify all configurable parameters
  • Document all possible values for each parameter
  • Identify invalid combinations (constraints)
  • Choose coverage strength based on risk (2-way is usually enough)

Generating tests:

  • Use a tool (allpairspy, ACTS, pict) not manual combination selection
  • Verify constraints are applied correctly
  • Generate tests in a format usable by your test framework

Executing tests:

  • Parameterize tests for easy reporting
  • Log which parameter combination failed
  • Review failures: which pairs were involved?

Conclusion

Combinatorial testing gives you high confidence in your coverage with a fraction of the test cases. Research consistently shows that pairwise (2-way) coverage catches 90%+ of bugs caused by parameter interactions—and you can achieve it with 10-30 tests instead of thousands.

Use combinatorial testing for:

  • Configuration testing (browsers, OSes, settings)
  • API parameter testing (multiple optional parameters)
  • Form validation (different field combinations)
  • Permission/role testing (user roles × feature flags × contexts)

The mathematical guarantee—every pair is covered—gives you confidence that your test suite will catch interaction-based bugs systematically, not by luck.

Read more

Start now free