Case file — TS3

Parameterised Scenarios

Generic steps, table data, property-based next.

The third near-duplicate scenario is the prompt to parameterise. The fifth row of an Examples table is the prompt to ask whether the property would carry the load. The discipline is the same one F3 Don't Repeat Yourself enforces on production code, on the testing surface.

ByAdam LewisPublished3 May 2026Reading11 minVersionv1.0ConfidenceHigh
§0b

Opinion

I've cleaned up too many feature files where the same scenario was copy-pasted forty times for forty currencies, each with the value hard-coded into the step text. The first developer wrote five scenarios because that's what the spec asked for; the next developer copied the pattern; six sprints later there are 200 scenarios and one bug-fix needs the same change applied 200 times. The rule is the same one F3 DRY enforces on production code: when the second near-duplicate appears, parameterise.1Andy Hunt & Dave ThomasThe Pragmatic Programmer (Addison-Wesley, 1999). Section 8 “The Evils of Duplication”: knowledge in a system should have a single, unambiguous, authoritative representation. The cross-pillar root for the parameterise-on-duplication rule. TS3 is the application of that rule inside the testing folder.

The two Cucumber primitives are siblings, not the same thing. A parameterised step definition (Given I am on the {string} page) collapses N near-duplicate step definitions into one, with the values inline.2Cucumber projectCucumber Expressions (github.com/cucumber/cucumber-expressions, 2017+). The parameter-type system: <code>{string}</code>, <code>{int}</code>, <code>{float}</code>, user-defined types. The mechanism that collapses near-duplicate step definitions into one. A Scenario Outline with an Examples table collapses N near-duplicate scenarios into one outline, with the values in a table.3Cucumber documentationGherkin Reference. The canonical doc for Scenario Outline + Examples tables. &ldquo;The keyword can be used to run the same Scenario multiple times, with different combinations of values.&rdquo; Both belong; both pull duplication out of the feature file at different altitudes. Most teams use one without thinking about the other.

The third rung is property-based testing (QuickCheck in 2000, Hypothesis in 2015, fast-check tracking both) and it is the rung most teams never climb.4Koen Claessen &amp; John Hughes&ldquo;QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs&rdquo; (ICFP 2000). The original property-based testing paper; defines the property + generator + shrinker triad TS3&rsquo;s third rung inherits. The Examples table is a hand-curated set of property points; a property-based test generates them. The team that writes 200 hand-rolled currency rows could write one property: for any positive amount in any supported currency, the round-trip through conversion and formatting preserves the precision the regulator requires . The generator finds the edge case the developer would not have written. Same axis, different rung.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

Generic steps, table-driven data, properties when the table is infinite. One parameterised step beats five near-duplicate scenarios; one Scenario Outline beats five copy-pasted features; one property-based test beats a thousand hand-written rows. Reach for the next rung when the current one starts costing more than it saves.

/tenet/parameterised-scenarios/TS3
§0c

AI eyes only

Rule: generic steps, table data, property-based next.

Reject: copy-paste scenarios with one-row variations. Reject: writing the fortieth currency scenario instead of an Outline. Reject: parameterising things that were duplicated for a reason.

Generate: at the second near-duplicate, convert to a Scenario Outline with an Examples table. At an open-ended set of cases, write a property-based test.

Diagnostic: count near-duplicate scenarios. Two is fine; the third is the trigger to parameterise.

§0d

Why?

  • Two hundred copy-pasted scenarios collapse into one outline plus a table. The diff that added the fortieth currency is one row, not forty PR comments.
  • The Examples table reads as the documentation. The header is the schema; each row is a scenario; the table doubles as a reference for non-developers.
  • Same operating principle as F3 Don't Repeat Yourself on the testing surface. The third near-duplicate is the trigger; before that, the duplication is too cheap to fix.
  • Property-based testing is the next rung up. When the Examples table would be infinite, the generator produces the rows the developer wouldn't have written and the shrinker reports the minimal counter-example.
  • Parameterised step definitions reduce step-file noise. One I am on the {string} page step replaces twelve near-identical step methods.
  • Coding agents reach for parameterisation when the rule is in front of them. The third copy-paste triggers a refactor rather than another forty.
  • The combinatorial coverage that hand-rolling cannot produce. A 6 × 6 table delivers thirty-six scenarios in the space of one outline; a property-based test delivers thousands.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

The Gherkin Scenario Outline keyword shipped in the original Cucumber release (Aslak Hellesøy, 2008) and is documented in Cucumber's reference grammar:3Cucumber documentationGherkin Reference. The canonical doc for Scenario Outline + Examples tables. &ldquo;The keyword can be used to run the same Scenario multiple times, with different combinations of values.&rdquo; “the keyword can be used to run the same Scenario multiple times, with different combinations of values.” The Examples table is part of the same primitive;<>-delimited parameters reference column headers, and Cucumber substitutes values before the step matches a definition. The mechanism is older than most Gherkin teams realise.

Parameterised step definitions arrived alongside the Cucumber Expressions overhaul (2017), with {string}, {int}, {float}, and user-defined parameter types in the step signature.2Cucumber projectCucumber Expressions (github.com/cucumber/cucumber-expressions, 2017+). The parameter-type system: <code>{string}</code>, <code>{int}</code>, <code>{float}</code>, user-defined types. The mechanism that collapses near-duplicate step definitions into one. Wynne and Hellesøy's Cucumber Book7Matt Wynne &amp; Aslak HellesøyThe Cucumber Book, 2nd ed. (Pragmatic Bookshelf, 2017). Ch. 6 Working with Step Arguments: the canonical guidance on when to parameterise; Hellesøy designed Cucumber Expressions. is the canonical reference; their guidance is to organise step definitions by domain concept, then parameterise inside each concept to avoid duplication.

Property-based testing — the third rung — predates BDD by half a generation. Koen Claessen and John Hughes's 2000 ICFP paper introduced QuickCheck for Haskell;4Koen Claessen &amp; John Hughes&ldquo;QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs&rdquo; (ICFP 2000). The original property-based testing paper; defines the property + generator + shrinker triad TS3&rsquo;s third rung inherits. David MacIver's Hypothesis (2015) ported the model to Python with shrinking-driven minimal counterexamples;8David R. MacIver&ldquo;What is Property-Based Testing?&rdquo; (Hypothesis docs, 2016+). The accessible introduction; the framing TS3 uses for the third rung. fast-check tracks both for the JavaScript ecosystem.9Nicolas Dubienfast-check (fast-check.dev, 2017+). The mainstream JavaScript implementation; tracks QuickCheck&rsquo;s combinator API and Hypothesis&rsquo;s shrinking model. The framing is consistent across implementations: where the example-based test asserts a specific input-output pair, the property-based test asserts a relation that holds for any input drawn from a generator.

The cross-pillar lineage runs through F3 DRY. The second near-duplicate is the trigger; TS3 is that rule applied to the testing surface, with three rungs scaled to the cost of the fix at each tier.

§2

Quotes

The Scenario Outline keyword can be used to run the same Scenario multiple times, with different combinations of values.

Cucumber Docs · Gherkin Reference

Property-based tests take these concrete scenarios and generalise them by focusing on which features of the scenario are essential and which are allowed to vary.

David R. MacIver · Hypothesis docs

QuickCheck encourages writing simple specifications of the program; the tool tests them by generating random input.

Koen Claessen & John Hughes · QuickCheck (2000)

Avoid writing similar step definitions, as they can lead to clutter.

Cucumber Docs · Step Organization
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Cucumber documentation · 2020+
    The canonical reference for the Scenario Outline keyword and the Examples table. &ldquo;The Scenario Outline keyword can be used to run the same Scenario multiple times, with different combinations of values.&rdquo;
  2. 02
    Cucumber project · 2017+
    Documents the parameterised-step half &mdash; <code>{string}</code>, <code>{int}</code>, <code>{float}</code>, user-defined parameter types. The mechanism that turns N near-duplicate step definitions into one.
  3. 03
    David R. MacIver · 2016+
    &ldquo;Property based testing is the construction of tests such that, when these tests are fuzzed, failures in the test reveal problems with the system under test that could not have been revealed by direct fuzzing.&rdquo; The framing TS3 uses for the third rung.
  4. 04
    Koen Claessen & John Hughes · 2000
    The original property-based testing paper. The framing TS3&rsquo;s third rung inherits: a property is a relation that holds for any input drawn from a generator.
  5. 05
    Matt Wynne & Aslak Hellesøy · 2017
    The reference text on operating parameterised steps. Hellesøy designed Cucumber Expressions; Wynne&rsquo;s chapter is the canonical guidance on when to parameterise.

Sixteen sources, three rungs. Cucumber's docs and Wynne & Hellesøy carry the in-feature mechanics. The MacIver / Hughes / Claessen line carries the property-based rung: QuickCheck in 2000, Hypothesis in 2015, fast-check tracking both. The qualifying voices further down carry the over-parameterisation steelman the reply addresses.

§4

Examples

Viewing: TypeScript.
Avoid
Filecapacity.spec.ts
// Before: five near-duplicate tests. One row per tier, hand-rolled.it("rejects rescue when sanctuary at capacity 10", () => {  expect(rejectsAtCapacity(fillSanctuary(10))).toBe(true);});it("rejects rescue when sanctuary at capacity 20", () => {  expect(rejectsAtCapacity(fillSanctuary(20))).toBe(true);});it("rejects rescue when sanctuary at capacity 30", () => {  expect(rejectsAtCapacity(fillSanctuary(30))).toBe(true);});it("rejects rescue when sanctuary at capacity 40", () => {  expect(rejectsAtCapacity(fillSanctuary(40))).toBe(true);});it("rejects rescue when sanctuary at capacity 50", () => {  expect(rejectsAtCapacity(fillSanctuary(50))).toBe(true);});
Prefer
Filecapacity.spec.ts
// After: one table-driven test. The rows are the spec.it.each([10, 20, 30, 40, 50])(  "rejects rescue when sanctuary at capacity %d",  (capacity: number) => {    expect(rejectsAtCapacity(fillSanctuary(capacity)(sanctuary).ok).toBe(true);  },);// Next rung: when the table would be infinite, generate the inputs.it("rejects every rescue when intake equals capacity", () => {  fc.assert(fc.property(fc.integer({ min: 1 }), (n) =>    rejectsAtCapacity(fillSanctuary(n)).ok === false));}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in .gherkin-lintrc. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
no-duplicate-scenario-namesgherkin-lintnear-duplicate scenarios with the same name (or names that differ only by data). The signal that an Outline + Examples table is overdue.
scenario-sizegherkin-lintscenarios over the steps-length threshold &mdash; usually a sign that a Background or a parameterised step is needed.
vitest it.eachVitestnear-duplicate <code>it()</code> blocks. <code>it.each</code> turns the duplicates into one row per dataset entry, with the cell values printed in the reporter.
fast-check fc.propertyfast-checkan Examples table that would be infinite. <code>fc.property</code> with arbitraries replaces the table with a generator; the shrinker reports the minimal counter-example.
fast-check fc.assertfast-checknon-deterministic property runs. <code>fc.assert</code> with a fixed seed in CI keeps the property reproducible; flake-rate stays inside the budget.
playwright-bdd Scenario Outline bindingplaywright-bddOutlines whose Examples table doesn&rsquo;t match the parameterised step signature. Type-safe binding refuses the build before the suite runs.
.gherkin-lintrcconfiguration snippet
{
  "no-duplicate-scenario-names": "error",
  "no-restricted-tags": ["error", { "tags": ["@wip"] }],
  "no-empty-background": "error",
  "no-superfluous-tags": "error",
  "scenario-size": ["warn", { "steps-length": { "Scenario": 12, "Background": 5 } }],
  "no-files-without-scenarios": "error",
  "name-length": ["error", { "Feature": 70, "Scenario": 70, "Step": 100 }]
}
§4c

AI rules

File.cursor/rules/ts3-parameterised-scenarios.mdc
---
description: Prickles TS3 — Parameterised Scenarios
globs: "**/*.{feature,spec.ts,test.ts,spec.tsx,test.tsx}"
alwaysApply: false
---

## Prickles TS3 — Parameterised Scenarios

One step definition with `{string}` placeholders beats five near-duplicate scenarios. The step is the noun; the parameter is the cell in the row.

Use Scenario Outlines with Examples tables for the in-Gherkin form. The header is the schema; each row is a scenario; the table is the documentation.

Reach for property-based testing when the table would be infinite. QuickCheck, Hypothesis, fast-check generate the rows you'd never have written by hand.

Three is the trigger. The third near-duplicate is the moment to parameterise; before that, the duplication is too cheap to fix.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

The honest steelman is parameter explosion. A Scenario Outline with twenty rows in the Examples table reads as a clean spec only until the next requirement adds a column. Now the table has 20 × 6 cells, the failure mode of any single row is harder to spot, and the parameterisation that paid off at five rows is debt at twenty. Vladimir Khorikov sharpens the point for unit tests5Vladimir KhorikovUnit Testing: Principles, Practices, and Patterns (Manning, 2020), ch. 3.4. The parameter-explosion smell: a parameterised test that hides variation behind data plumbing produces failure messages that are tuples rather than sentences.: parameterised tests can hide the actual variation behind data plumbing, so the failure message stops being a sentence and starts being a tuple. Marick's checking-versus-testing distinction6Brian Marick&ldquo;Coverage-Driven Test Design&rdquo; (exampler.com, 2003). The checking-versus-testing distinction sharpened for parameterisation: a table that passes is a regression net, not a probe. bites here too: a parameterised scenario that passes the table is a regression net, not a probe; the developer stops asking “what other rows would matter?”

§6

Counter-argument retort

Reply

Khorikov's parameter-explosion objection5Vladimir KhorikovUnit Testing: Principles, Practices, and Patterns (Manning, 2020), ch. 3.4. The parameter-explosion smell: a parameterised test that hides variation behind data plumbing produces failure messages that are tuples rather than sentences. is conceded inside its scope and refused outside it. Yes, a 20-row Examples table that grows a sixth column becomes harder to read; the answer isn't to abandon parameterisation but to escalate — either split the table along the new dimension into two outlines, or promote the assertion to a property-based test that draws inputs from a generator and lets a shrinker find the minimal failing case. The over-parameterisation smell is the trigger for the next rung, not the trigger to retreat to copy-paste.

Marick's checking-versus-testing concern6Brian Marick&ldquo;Coverage-Driven Test Design&rdquo; (exampler.com, 2003). The checking-versus-testing distinction sharpened for parameterisation: a table that passes is a regression net, not a probe. is the deeper one and the reply concedes it explicitly. A parameterised scenario “passes the table” rather than “asks new questions”. Property-based testing restores the questioning lens: the generator produces inputs the developer hadn't imagined, and the shrinker reports the minimal counter-example. The third rung is what TS3 actually needs; the table is the second rung that earns its place by paying off long enough to be worth keeping.

The over-parameterisation pathology is real and avoidable. Two heuristics keep the table honest. First, every column has to vary independently — if columns A and B always move together, they're one parameter and the table has the wrong shape. Second, the failure message has to read as a sentence — if the assert prints a tuple, parameterise the assert too, or split the outline. Both heuristics push toward the property-based form when the table reaches the ceiling of what an outline can carry.

The discipline reduces to three lines on the wall: parameterise the second near-duplicate, table the second Outline that earns it, generate when the table goes infinite. Each rung pays at a different threshold; the trigger is the cost of the next change, not the count of the current rows. The classic “wait for three” counter is acknowledged and rejected — in the AI era, the agent reads the second near-duplicate before the human does and the cost of waiting compounds.

§7

Notes

  1. [1]Andy Hunt &amp; Dave ThomasThe Pragmatic Programmer (Addison-Wesley, 1999). Section 8 &ldquo;The Evils of Duplication&rdquo;: knowledge in a system should have a single, unambiguous, authoritative representation. The cross-pillar root for the parameterise-on-duplication rule.
  2. [2]Cucumber projectCucumber Expressions (github.com/cucumber/cucumber-expressions, 2017+). The parameter-type system: <code>{string}</code>, <code>{int}</code>, <code>{float}</code>, user-defined types. The mechanism that collapses near-duplicate step definitions into one.
  3. [3]Cucumber documentationGherkin Reference. The canonical doc for Scenario Outline + Examples tables. &ldquo;The keyword can be used to run the same Scenario multiple times, with different combinations of values.&rdquo;
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27