skip to content

In pairwise test generation, how do you handle forbidden parameter combinations?

level: middleimportance: should knowfreq 52%

answer

  1. some rows cannot be built at all
  2. tell the generator, do not delete rows
  3. deleting a row loses its other pairs
  4. impossible is not the same as invalid
  5. constraints can raise the row count

basics

~20 s

Declare forbidden combinations as constraints in the model so the generator never emits them and never counts their pairs as owed. Deleting offending rows afterwards silently destroys coverage of legitimate pairs that shared those rows.

solid answer

~50 s

A configuration model almost always contains combinations that cannot exist — a product line that no rating region sells, a cadence unavailable on one sales channel. The right handling is to express them as **constraints** given to the generator up front, in the form "if this value, then never that value". The generator then excludes those cells from the pool of pairs it owes and builds an array whose every row is constructible. The wrong handling is to generate freely and then delete the rows that look impossible: each deleted row was also carrying up to fourteen other perfectly valid pairs, so you lose coverage without any report telling you so. Keep two categories apart: a **forbidden combination** cannot be built at all and belongs in the constraints, while an **invalid input** can be built and must be exercised so you can check it is rejected.

code

pseudocode · 18 lines
pseudocode
model = {
    "product_line": ["personal_auto", "home", "renters", "umbrella"],
    "cadence":      ["monthly", "quarterly", "annual"],
    "region":       ["R1", "R2", "R3", "R4", "R5", "R6", "R7"],
    "channel":      ["direct", "broker", "partner"]
}

constraints = [
    # R3 and R6 hold no umbrella licence
    when(product_line == "umbrella") forbid(region in ["R3", "R6"]),
    # the partner channel bills only on a yearly cycle
    when(channel == "partner") forbid(cadence in ["monthly", "quarterly"])
]

array = generate_covering_array(model, strength = 2, constraints = constraints)

assert every_row_satisfies(array, constraints)
assert uncovered_reachable_pairs(array, model, constraints) == 0

go deeper

for a junior

Know that some parameter combinations simply cannot exist, and that the way to deal with them is to tell the generator up front rather than to remove rows from its output afterwards.

for a middle

Explain the mechanics: implication-style constraints, why deleting a row destroys unrelated pairs, why constrained models can grow rather than shrink, and how the coverage report should count only reachable pairs.

for a senior

Show judgement about categories. Distinguish unconstructible states from inputs the system must reject, keep negative cases in the suite, and treat a rule that suddenly makes many rows infeasible as a domain change to investigate.

for a principal

Own the modelling strategy: when dense constraints mean the model is really two products, how sub-models keep each array reviewable, and how you version constraints with their reasons so they do not outlive the rule that created them.

## Why a raw cross-product model is usually a lie A parameter model written as independent lists implies that every value of every parameter can coexist with every value of every other. Real configuration spaces rarely honour that. In the quote engine model — product line, payment cadence, rating region, prior-claims flag, sales channel, discount bundle — two rating regions do not sell the umbrella product line at all, and the monthly cadence is not offered through the partner channel. Handing that model to a generator unmodified produces rows that cannot be set up, and a suite that cannot be set up gets quietly skipped, edited by hand, or marked as an expected failure until nobody reads the report. ## Constraints belong in the model, not in a post-filter The correct mechanism is to declare the exclusions as **constraints** before generation, typically as implications: `product_line = umbrella => region not in {R3, R6}`. Two things then happen. First, the generator never emits a row that violates the rule. Second — and this is the part candidates miss — the excluded pairs are removed from the set of pairs the array is *obliged* to cover, so the coverage report stays honest: it reports coverage of the reachable pair space rather than an unreachable ideal that will never hit 100%. Contrast the post-filter approach: generate 43 rows, notice that six of them are impossible, delete them, ship 37. Each deleted row was also carrying up to fourteen valid pairs across the other parameter pairings, and some of those pairs appeared in no other row. You have created uncovered pairs and, worse, no artefact records which ones. The set still looks like a covering array and is no longer one. If a generator you use cannot accept constraints, the salvage is to re-run coverage analysis over the surviving rows and append rows for whatever is now missing — not to ship the truncated set. ## Forbidden versus invalid: the distinction interviewers probe A **forbidden combination** is unconstructible: the system offers no path that produces it, so there is no behaviour to observe and nothing to assert. It goes in the constraints. An **invalid input** is constructible and must be rejected. A prior-claims flag set on a product line that does not price claims history is not impossible to submit; it is something the quote engine must refuse with a specific message. Constraining it away deletes a whole class of negative testing. The test for which category you are in is simple: can a user or an upstream caller cause this state? If yes, it is not forbidden, and the covering array should carry it. There is a third category worth naming: combinations that are *possible but out of scope for this run* — a legacy channel being retired at the end of the 3-week release train, say. Model those as a temporary constraint with a note and an expiry, not as a permanent fact about the domain, or the constraint outlives its reason and quietly shrinks the matrix forever. ## Second-order effects of constraints Adding constraints does not always shrink the output. Exclusions break up the packing: values that used to travel together in one row now cannot, so the generator needs extra rows to place the pairs it can no longer combine. Seeing a row count rise from 43 to 47 after adding four constraints is normal and is not a bug. Heavily constrained models also risk **infeasible pairs** — a pair that every constraint set forbids in combination with something else, so it can never appear. A good generator reports these explicitly. If your tool does not, sanity-check the coverage report against the declared pair count rather than trusting a bare "100% covered" line. Where constraints become dense, an alternative is to split the model into **sub-models**: one parameter set for the personal product lines, another for commercial, each internally consistent, each generating its own array. Two clean arrays are easier to defend and to regenerate than one model carrying twenty implications, and the union usually costs fewer rows than the constrained single model. ## Practical hygiene Keep constraints in the same versioned file as the parameter model so a reviewer can see the domain rules and the value lists together. Write each constraint with a one-line reason; "region R3 has no umbrella licence" survives a re-read months later in a way that a bare exclusion does not. Re-derive the array when the rules change rather than patching rows by hand, and diff the generated set so you can see which configurations entered and left the suite. And treat a constraint that suddenly makes many rows infeasible as a signal that the domain changed, not that the generator misbehaved.

  • You add four constraints and the generated row count goes up rather than down. Is that a defect?
    No, it is expected. Constraints stop the generator packing certain values into the same row, so pairs that previously shared a row now need rows of their own. A model whose exclusions cut across many parameters can easily produce a larger array than the unconstrained version. What would be a defect is a row that violates a declared constraint, or a coverage report claiming full coverage while reachable pairs remain unplaced.
  • How do you keep negative testing alive when a model is heavily constrained?
    Separate unconstructible states from rejectable ones. Only the first belong in constraints; the second are legitimate configurations whose expected outcome is a specific rejection, so they stay in the model with an oracle that asserts the refusal rather than a successful result. If the generator cannot express "this row should fail", keep those cases in a small companion set alongside the array rather than dropping them.
  • When would you split one constrained model into several sub-models instead?
    When the constraints stop being exceptions and start describing two different products. If most implications fire for one product line and never for another, the parameters are not really shared, and one array is being distorted to serve both. Two sub-models, each internally consistent, usually generate fewer total rows, read far better in review, and can be regenerated independently when only one side's rules change.

saying these in an interview costs you the question

  • Deletes impossible rows from the generated set afterwards
  • Treats every rejectable input as a forbidden combination
  • Expects constraints to always reduce the row count
  • Edits generated rows by hand instead of regenerating
  • Reports full coverage without checking reachable pairs
  • Keeps constraints in a separate place from the value lists

context