Applying Combinatorial Testing to Configuration Testing

Applying Combinatorial Testing to Configuration Testing

Configuration testing is where combinatorial explosion is most brutal and most avoidable. A web application deployed across three operating systems, four browsers, two database versions, three API versions, and two CDN configurations has 3 × 4 × 2 × 3 × 2 = 144 environment configurations. Add a feature flag with three states and you're at 432. Add a deployment region with five options and you're at 2,160.

Nobody is running 2,160 configurations in their test suite. The question is whether you're sampling those configurations randomly — which is what most teams do, implicitly — or sampling them systematically using combinatorial methods.

This post covers the configuration explosion problem in detail, how combinatorial testing tames it, and a complete worked example for a web application.

The Configuration Explosion Problem in Practice

Configuration variables in production systems come from multiple layers:

Infrastructure layer: Cloud provider, region, instance type, OS version, kernel version.

Platform layer: Runtime version (Node 18 vs 20, Python 3.10 vs 3.11), database engine and version (PostgreSQL 14 vs 15, MySQL 8.0 vs 8.4), cache backend (Redis 6 vs 7), message queue.

Application layer: Feature flags, A/B test variants, locale/language/timezone settings, user permission level.

Client layer: Browser, browser version, device type, screen resolution, network speed, accessibility settings.

Integration layer: Third-party API version, authentication provider, payment gateway, CDN.

Each layer has multiple parameters. Each parameter has multiple values. The product of all possible combinations is the full configuration space — and it's routinely in the millions for production systems.

Why "Test the Important Combinations" Fails

The instinctive response to configuration explosion is to test "the most common" or "the most important" combinations. In practice this means:

  • The QA lead picks combinations based on intuition
  • The same combinations get tested every release (because that's what's documented)
  • New combinations are added only when a customer reports a bug in one
  • The combinations tested skew toward the developer's local setup

This approach has a systematic flaw: it covers the same space repeatedly while leaving other regions unexplored. You're not sampling the configuration space — you're sampling the team's mental model of the configuration space. Bugs that don't fit that mental model don't get found.

Combinatorial testing replaces this with a mathematical guarantee: every pair (or triple) of configuration values has been exercised together. No combination class gets implicitly skipped.

A Worked Example: Web Application Configuration Testing

Let's work through a concrete example. You're QA lead for a SaaS web application. You need to design the configuration test matrix for a major release.

Step 1: Enumerate All Configuration Parameters

Talk to the developers, ops team, and support team. Document every dimension that could affect behavior:

Parameter Values
OS Ubuntu 22.04, RHEL 9, Windows Server 2022
Node.js version 18 LTS, 20 LTS, 22
PostgreSQL version 14, 15, 16
Browser Chrome, Firefox, Safari, Edge
Browser version Current, Current-1
Network LAN, WiFi, 4G, 3G
Feature flag: new_checkout enabled, disabled
Feature flag: dark_mode enabled, disabled
Cache backend Redis 6, Redis 7
CDN Cloudflare, CloudFront, none
Auth provider Internal, Auth0, Okta
Locale en-US, de-DE, ja-JP

12 parameters, values ranging from 2 to 4. Full coverage: 3×3×3×4×2×4×2×2×2×3×3×3 = 93,312 configurations.

No team on earth is running 93,312 configurations. With pairwise testing, you can cover this space with approximately 65–75 test configurations.

Step 2: Model Constraints

Some combinations are invalid or redundant. Model them explicitly before generating:

# PICT model file: webapp-config.txt

OS: Ubuntu, RHEL, WinServer
NodeVersion: 18, 20, 22
PostgreSQL: 14, 15, 16
Browser: Chrome, Firefox, Safari, Edge
BrowserVersion: Current, Current-1
Network: LAN, WiFi, 4G, 3G
CheckoutFlag: enabled, disabled
DarkModeFlag: enabled, disabled
CacheBackend: Redis6, Redis7
CDN: Cloudflare, CloudFront, none
AuthProvider: Internal, Auth0, Okta
Locale: en-US, de-DE, ja-JP

# Constraints
# Safari only on macOS-like environments (exclude WinServer)
IF [Browser] = "Safari" THEN [OS] <> "WinServer";

# Legacy browser version less important for modern Node
IF [BrowserVersion] = "Current-1" THEN [NodeVersion] <> "22";

# Redis 7 only tested with PostgreSQL 15+
IF [CacheBackend] = "Redis7" THEN [PostgreSQL] <> "14";

With constraints applied, PICT will avoid generating nonsensical combinations like Safari/Windows Server.

Step 3: Generate the Test Matrix

pict webapp-config.txt /o:2 > test-matrix.tsv

Review the output. With 12 parameters and the constraints above, expect approximately 65–80 test cases. Each row is one complete configuration to test.

pict webapp-config.txt /o:2 /s
# Prints statistics: number of cases, pairs covered, etc.

If you want 3-way coverage on a subset of the highest-risk parameters, you can split the model. Define a sub-model for the critical infrastructure parameters:

# infra-submodel.txt (for 3-way coverage)
OS: Ubuntu, RHEL, WinServer
NodeVersion: 18, 20, 22
PostgreSQL: 14, 15, 16
CacheBackend: Redis6, Redis7
pict infra-submodel.txt /o:3 >> test-matrix.tsv

Merge the outputs, deduplicate, and you have a hybrid test matrix: 2-way across all 12 parameters, 3-way within the critical infrastructure group.

Step 4: Prioritize the Matrix

Not all 70 test configurations are equally important. Sort by risk:

  1. Highest priority: Configurations involving older or less-common platforms (RHEL, Node 18, PostgreSQL 14, Edge, Okta). These are more likely to harbor compatibility bugs.
  2. Medium priority: Common configurations with feature flags in non-default states.
  3. Lower priority: Configurations testing well-established parameter combinations that rarely change.

Run the high-priority subset first. If CI time allows, run the full matrix. If not, at least the highest-risk configurations get coverage.

Step 5: Map to Automated Tests

Configuration testing without automation is expensive. The goal is to run these 70 configurations automatically in CI, not manually.

The pairwise-generated matrix becomes a data file that drives parameterized automated tests. In a Docker-based setup, each row in the matrix maps to a container configuration:

# docker-compose template
services:
  web:
    image: myapp:latest
    environment:
      NODE_VERSION: ${NODE_VERSION}
      FEATURE_CHECKOUT: ${CHECKOUT_FLAG}
      AUTH_PROVIDER: ${AUTH_PROVIDER}
      LOCALE: ${LOCALE}
  db:
    image: postgres:${POSTGRES_VERSION}
  cache:
    image: redis:${REDIS_VERSION}

A CI script reads the test matrix TSV and spins up each configuration in turn:

import csv
import subprocess

with open('test-matrix.tsv') as f:
    reader = csv.DictReader(f, delimiter='\t')
    for config in reader:
        env = {
            'NODE_VERSION': config['NodeVersion'],
            'POSTGRES_VERSION': config['PostgreSQL'],
            'CHECKOUT_FLAG': config['CheckoutFlag'],
            'AUTH_PROVIDER': config['AuthProvider'],
            'LOCALE': config['Locale'],
            # ... etc
        }
        result = subprocess.run(
            ['docker-compose', 'up', '--abort-on-container-exit', '--exit-code-from', 'tests'],
            env={**os.environ, **env}
        )
        if result.returncode != 0:
            print(f"FAILED: {config}")
            failed_configs.append(config)

This is configuration testing at scale: systematic, automated, reproducible, and based on mathematical coverage guarantees rather than intuition.

The Feature Flag Combinatorial Problem

Feature flags deserve special attention. A system with 20 feature flags has 2^20 = 1,048,576 possible flag combinations. Testing all of them is impossible. But you also can't just test "all on" and "all off" — the point of flags is that subsets are enabled for different users.

Combinatorial testing handles this directly. Model the flags as parameters:

FlagA: on, off
FlagB: on, off
FlagC: on, off
FlagD: on, off
FlagE: on, off
# ... etc for 20 flags

All-pairs coverage of 20 binary flags requires approximately 10–12 test cases (because all-pairs of binary parameters is very compact — each test case covers many pairs simultaneously). This is feasible.

But there's a subtlety: not all pairs of flags are equally likely to interact. Flags that control the same feature area or that share code paths are more likely to interact. You can address this by:

  1. Grouping related flags and applying 3-way coverage within each group
  2. Adding seeds for known-sensitive combinations (e.g., "new checkout" + "loyalty program" flags together)
  3. Using constraint syntax to exclude logically impossible combinations (e.g., Flag A requires Flag B to be on)

Platform Compatibility Matrices

Browser/OS compatibility testing is a classic configuration problem, and one where pairwise testing has an established track record.

A typical compatibility matrix for a consumer web app:

Parameter Values
Browser Chrome, Firefox, Safari, Edge, Samsung Internet
Browser version Latest, Latest-1, Latest-2
OS Windows 10, Windows 11, macOS Ventura, macOS Sonoma, Ubuntu 22.04
Device type Desktop, Tablet, Mobile
Screen size 1920×1080, 1440×900, 1280×800, 375×812 (mobile)
Connection Broadband, 4G, 3G

Full coverage: 5 × 3 × 5 × 3 × 4 × 3 = 2,700 combinations.

All-pairs: approximately 35–40 combinations.

That's the difference between a compatibility test pass that takes a week and one that takes an afternoon.

Important: apply constraints. Samsung Internet only appears on Android. Safari on iOS and macOS behaves differently. Mobile devices don't have 1920×1080 screens. Model these constraints to avoid wasting test cases on impossible configurations.

Database Version Upgrade Testing

When upgrading database versions in production, you need to test the application against both the old and new version across multiple application versions. This is a classic multi-version compatibility matrix:

Parameter Values
DB version PostgreSQL 14, 15, 16
App version v2.3, v2.4, v3.0
Migration state pre-migration, post-migration, mid-migration-rollback
Data volume small (1K rows), medium (100K rows), large (10M rows)
Connection pooling PgBouncer, direct
Read replicas 0, 1, 3

Full coverage: 3×3×3×3×2×3 = 486 combinations.

All-pairs: approximately 25 combinations.

The all-pairs suite will cover every (DB version, migration state) pair, every (app version, data volume) pair, every (connection pooling, read replica count) pair — and all other pairs. What it won't guarantee is that every three-way combination (DB version, migration state, data volume) is covered. For a database upgrade — a risky operation — you might step up to 3-way for the most critical sub-group of parameters.

Workflow Integration: Making It Stick

The operational challenge with configuration testing isn't generating the matrix — it's maintaining it as the system evolves.

Treat the PICT model file like code. Check it into the repository alongside the tests. Update it when parameters change. Review changes to the model file in pull requests.

Regenerate the matrix automatically. Make test matrix generation a CI step, not a manual activity. When the model file changes, the matrix regenerates automatically.

Track configuration coverage over time. Keep a record of which configurations have been tested in recent releases. If a parameter value hasn't been tested in three releases, that's a risk to flag.

Document why constraints exist. Comments in the model file explaining why a constraint exists save significant debugging time when the constraint becomes outdated.

# Safari runs on macOS and iOS only — Windows/Linux builds don't include WebKit
IF [Browser] = "Safari" THEN [OS] = "macOS" OR [OS] = "iOS";

The Return on Investment

The math on configuration testing ROI is straightforward. If manually running a configuration takes 30 minutes, running 2,700 configurations takes 1,350 hours — not feasible. All-pairs drops it to 18 hours, which is feasible across a small team over two days.

With automated configuration testing, those 40 all-pairs configurations might take 4 hours of CI time. Running them on every release candidate is now practical.

The bugs found per test case in configuration testing tend to be high-severity. Configuration incompatibilities often manifest as complete failures — "the app doesn't start" or "authentication broken for all Okta users" — not subtle behavioral differences. The defect density per configuration test case tends to be higher than for functional test cases. That's further justification for investing in proper configuration test coverage.

For teams using cloud-hosted testing platforms like HelpMeTest, configuration matrices can be executed across environments without managing your own infrastructure. Usage-based pricing at $0.003 per test run provides automated test execution that can be driven by parameterized configuration data from your PICT-generated matrix.

Summary

Configuration testing without combinatorics is random sampling you don't know is random. Every team says "we test the important combinations" and every team eventually discovers a production bug in a combination nobody thought to test.

Pairwise combinatorial testing converts this from an intuition-based activity to an engineering discipline: enumerate parameters, model constraints, generate the minimal covering test set, automate execution, maintain the model over time. The test suite size stays manageable as the parameter space grows, the coverage guarantee is mathematical, and the maintenance overhead is low.

The barrier to entry is low: a PICT model file, a one-line command, and an afternoon to set up the automation pipeline.

Read more

Start now free