← Math in Practice
Concept Quantities and representation

Counting and combinatorics

A test matrix grows by multiplication; a useful plan starts by counting what is possible.

A team is preparing a release across three clients, three account plans, two regions, two login methods, and a feature flag. “We can test the combinations” sounds simple until the team counts them. Work out the size of the space first; then decide which combinations deserve a test and what a reduced test suite can actually promise.

The judgment to keep

Count the choices before scheduling tests. Multiply independent dimensions, subtract only combinations ruled out by explicit constraints, then choose coverage based on risk. A smaller suite is a sampling strategy, not proof that every interaction works.

TypeScriptGo Product rule · constraints · permutations and combinations · pairwise coverage
01 / Read the test matrix

Every independent choice can multiply the work.

A release engineer is preparing a customer portal update. The service supports three clients (web, iOS, Android), three plans (free, pro, enterprise), two regions (US, EU), two login methods (password, SSO), and a binary export flag (off, on). A test configuration chooses one value from each dimension.

The question is not “How many test cases do we usually write?” It is “How many distinct configurations are in scope if every choice can combine with every other choice?” That count is the size of the exhaustive test space. It gives the team a baseline before it decides whether exhaustive execution is affordable.

Case file / Customer portal release Five dimensions, with rules that make some combinations invalid.
Client
Web · iOS · Android
Plan
Free · Pro · Enterprise
Region
US · EU
Login
Password · SSO
Export
Off · On
Keep: list the dimensions and each allowed value before estimating the number of tests.
02 / Count the combinations

The product rule turns a list of choices into a total.

If a configuration makes one choice from each dimension, and all choices are initially allowed to combine, multiply the number of values: 3 × 3 × 2 × 2 × 2 = 72. This is the product rule. For each of the 3 clients there are 3 plans; for each client-plan pair there are 2 regions; and so on. Each new independent choice multiplies the number of partial configurations already counted.

3 clients × 3 plans × 2 regions × 2 logins × 2 flag states = 72

The multiplication assumes the dimensions are separate choices and every value can be paired with every value in the next dimension. “Independent” here means combinatorially unrestricted; it does not claim that user behavior, failures, or measured outcomes are statistically independent. Compatibility rules will change the count in the next step.

Some test tasks instead ask how many ways to select or order items. If a team chooses 2 providers from 4 for a failover pair and order does not matter, it uses a combination: C(4, 2) = 4! ÷ (2! × 2!) = 6. If primary and standby roles matter, there are 4 × 3 = 12 ordered pairs (a permutation). Decide whether “first” and “second” represent distinct roles before choosing the formula.

Unconstrained count · multiply each new dimension
After addingChoices so farPartial count
Client33
Plan3 × 39
Region9 × 218
Login18 × 236
Export flag36 × 272
03 / Apply real constraints

Compatibility rules remove cases; they do not justify guessing.

The product count includes combinations the product does not permit. For this example, SSO is available only on Pro and Enterprise plans, and export is available only in the web client. Treat each as an explicit constraint, then count the remaining cases by plan and client rather than subtracting a vague percentage.

First apply the SSO rule. The Free plan has one valid login value; Pro and Enterprise each have two. Across 3 clients, 2 regions, and 2 export states, that gives (1 + 2 + 2) × 3 × 2 × 2 = 60. Equivalently, 12 of the original 72 rows are invalid: Free + SSO for each client, region, and export state.

Now apply the web-only export rule. For each mobile client, there are 5 allowed plan-login combinations and 2 regions; the enabled-export state is invalid, removing 5 × 2 = 10 rows per mobile client. Remove 20 from 60 and the valid exhaustive space is 40. You can also check directly: web has 5 × 2 × 2 = 20 valid rows, and each mobile client has 5 × 2 × 1 = 10, for 20 + 10 + 10 = 40.

Illustrative constraints applied in order
StepCalculationCount left
All values unrestricted3 × 3 × 2 × 2 × 272
SSO only for paid plans(1 + 2 + 2) × 3 × 2 × 260
Export only on webWeb: 20 · iOS: 10 · Android: 1040
Check: independently derive the valid count a second way. The direct client breakdown also gives 40.
04 / Choose a coverage strategy

Testing every row is one choice, not the only defensible plan.

If running all 40 valid configurations is affordable and the release risk warrants it, exhaustive coverage is the clearest plan: every modeled row gets a test. If that is too slow or expensive, a team may use pairwise coverage: choose a smaller set of valid configurations so every feasible pair of values across every pair of dimensions appears at least once.

Why pairwise? It gives the team a systematic baseline for interactions between two choices—for example, EU + SSO or Android + Pro—while using fewer rows than the full product. The constraints matter: a pair that cannot occur, like Free + SSO, is not a feasible obligation. A pairwise generator must understand the same constraints as the application.

Pairwise coverage does not guarantee that every bug is found, or even that every three-way interaction is tested. A failure may require a particular combination of client, plan, and region; it may depend on a migration sequence, stale cache, data shape, or load that a static configuration row does not express. Use risk analysis and production evidence to decide where to add targeted cases, and retain end-to-end, boundary, and negative tests where they matter.

The word combination also appears in a precise counting formula. Choose k unordered items from n with C(n,k) = n! / (k!(n-k)!). A test matrix usually uses the product rule because it selects one value from each named factor. Use the combination formula for a separate subset-selection question; use permutations when order or roles change the outcome.

05 / Practice in code

Enumerate the valid rows and keep the rules visible.

The examples build the five dimensions, evaluate the two compatibility constraints, and count the rows that remain. Trace one rejected case—Free + SSO—and one rejected case—iOS + export enabled—through the predicate. Both languages report 72 raw combinations and 40 valid configurations.

Compare the same constrained count in TypeScript and Go.

Both implementations enumerate the same illustrative feature matrix.

TypeScriptFeature test matrix · product rule and constraints
configurations.ts
const clients = ['web', 'ios', 'android'] as const;
const plans = ['free', 'pro', 'enterprise'] as const;
const regions = ['us', 'eu'] as const;
const logins = ['password', 'sso'] as const;
const exportEnabled = [false, true] as const;

let raw = 0;
let rejectedBySso = 0;
let rejectedByExport = 0;
let valid = 0;

for (const client of clients) {
	for (const plan of plans) {
		for (const region of regions) {
			for (const login of logins) {
				for (const hasExport of exportEnabled) {
					raw += 1;

					if (plan === 'free' && login === 'sso') {
						rejectedBySso += 1;
						continue;
					}

					if (client !== 'web' && hasExport) {
						rejectedByExport += 1;
						continue;
					}

					valid += 1;
				}
			}
		}
	}
}

console.log({ raw, rejectedBySso, rejectedByExport, valid });
GoFeature test matrix · product rule and constraints
configurations.go
package main

import "fmt"

func main() {
	clients := []string{"web", "ios", "android"}
	plans := []string{"free", "pro", "enterprise"}
	regions := []string{"us", "eu"}
	logins := []string{"password", "sso"}
	exportStates := []bool{false, true}

	raw, rejectedBySSO, rejectedByExport, valid := 0, 0, 0, 0
	for _, client := range clients {
		for _, plan := range plans {
			for regionIndex := 0; regionIndex < len(regions); regionIndex++ {
				for _, login := range logins {
					for _, hasExport := range exportStates {
						raw++

						if plan == "free" && login == "sso" {
							rejectedBySSO++
							continue
						}

						if client != "web" && hasExport {
							rejectedByExport++
							continue
						}

						valid++
					}
				}
			}
		}
	}

	fmt.Printf("raw=%d rejected_by_sso=%d rejected_by_export=%d valid=%d\n", raw, rejectedBySSO, rejectedByExport, valid)
}
06 / Plan the next test

Carry the count into a system with one more interaction.

Practice / Payment regression matrix Choose what to cover when a payment path has multiple choices.
Payment method
Card · bank transfer · wallet
Currency
USD · EUR
Customer state
New · returning
Promotion
Off · on
Task
Count the unconstrained space, name a plausible constraint, then propose coverage and its limit.
Show a worked answer

The product rule gives 3 × 2 × 2 × 2 = 24 combinations before constraints. Suppose promotion is unavailable for bank transfer. There are 2 currencies × 2 customer states = 4 bank-transfer rows with promotion on, so the rule leaves 20 valid configurations.

If exhaustive runs are too costly, select tests so every feasible pair—method with currency, method with customer state, and so on—appears. Add targeted tests for discounts, refunds, and any high-risk workflow. Pairwise coverage does not exercise every four-way combination or prove the payment outcome is correct; assertions and realistic backend behavior still matter.

Take with you: define the dimensions, multiply the raw choices, encode real constraints, then make a precise coverage claim that matches the tests you ran.