bdd-step-library-curator · diff
git:20260724.d4db756 to git:20260725.b4ad92d
78 added, 57 removed. Audit A to A.
---
name: bdd-step-library-curator
- description: "Build-an-X workflow that keeps step definitions DRY across a Cucumber / Behave / Reqnroll project - periodically inventories step definitions, finds duplicates (different patterns matching the same intent), suggests consolidation, organizes by domain, and publishes a step library reference doc the team uses for \"is there already a step for X?\" before authoring new ones. Use as the antidote to step-definition proliferation in long-lived BDD projects."
+ description: "Keeps a BDD step-definition library DRY across a Cucumber / Behave / Reqnroll project - inventories every step definition, detects duplicates (different patterns matching the same intent), recommends canonical consolidations, reorganizes steps by domain, and publishes a step-library README the team greps for \"is there already a step for X?\" before authoring new ones. Use on a cadence as the antidote to step-definition proliferation in long-lived BDD projects, or when a new engineer reports they cannot find an existing step."
---
# bdd-step-library-curator
## Overview
After 6 months of a BDD project, step definitions proliferate:
- Two engineers write `Given a user` and `Given a logged-in user`
for the same fixture.
- Three slightly-different "I click X" steps (one for buttons,
one for links, one for arbitrary elements) - all do the same
thing.
- A new engineer can't find existing steps and writes a fourth
variant.
The result: step library bloat → Gherkin features hard to read,
step ambiguities, runtime errors, drift.
This skill builds a curation workflow.
## When to use
- A BDD project's step count exceeds ~50 (proliferation threshold).
- A new engineer reports "I couldn't find a step for X."
- Quarterly: scheduled step review.
- Before adopting BDD across multiple teams (proactively design
the step library shape).
- ## Step 1 - Inventory step definitions
-
- Per-language extraction:
+ ## How to use
- ```bash
- # Cucumber-JVM
- grep -rE '@(Given|When|Then|And|But)\(' src/test/java/ | \
- sed -E 's/.*@(Given|When|Then|And|But)\("([^"]*)".*/\2/'
+ 1. **Inventory** every step definition across the project - extract
+ patterns per runner (the per-runner commands are in
+ [references/step-extraction-and-overlap.md](references/step-extraction-and-overlap.md)).
+ 2. **Detect** duplicate / overlapping patterns by normalizing wording
+ and grouping - the overlap script lives in the same reference.
+ 3. **Recommend** one canonical step per duplicate group; deprecate the rest.
+ 4. **Reorganize** step files by domain and **publish** the step-library
+ README - the team's "is there a step for X?" index.
+ 5. **Gate** new steps at pre-merge so every added definition is acknowledged.
+ 6. **Repeat** on a cadence (quarterly plus threshold-triggered reviews).
- # Behave
- grep -rE '^@(given|when|then|step)\(' features/steps/ | \
- sed -E 's/.*@(given|when|then|step)\("([^"]*)".*/\2/'
+ ## Step 1 - Inventory step definitions
- # Reqnroll / SpecFlow
- grep -rE '\[(Given|When|Then|And|But)\(' Tests/Steps/ | \
- sed -E 's/.*\[(Given|When|Then|And|But)\("([^"]*)".*/\2/'
- ```
+ Extract every declared step pattern across the project. Each runner
+ uses its own annotation syntax (`@Given` in Cucumber-JVM, `@given`
+ in Behave, `[Given]` in Reqnroll / SpecFlow) - the per-runner
+ extraction commands are in
+ [references/step-extraction-and-overlap.md](references/step-extraction-and-overlap.md).
- Output: a list of step patterns. The audit:
+ The inventory produces a step audit:
```
Total step definitions: 142
Unique patterns: 138 (4 ambiguous duplicates already)
Per Gherkin verb:
Given: 58
When: 34
Then: 47
And: 3
```
## Step 2 - Detect duplicates / overlaps
Two patterns are likely duplicates when:
- Same wording, different phrasing: `Given a user` vs `Given an
existing user` vs `Given the user`.
- Same parameters, different verbs: `When I click {label}` vs
`When I press {label}` vs `When I tap {label}`.
- Same fixture, different shape: `Given a cart with {n} items` vs
`Given a cart containing {n} items`.
- ```python
- # scripts/step-overlap.py
- import re
-
- def normalize(pattern):
- """Lower; strip articles; remove parameter type hints."""
- p = pattern.lower()
- p = re.sub(r'\b(a|an|the)\b', '', p)
- p = re.sub(r'\{[^}]+\}', '{var}', p)
- p = re.sub(r'\s+', ' ', p).strip()
- return p
-
- steps = [...] # from Step 1
-
- normalized = {}
- for s in steps:
- n = normalize(s['pattern'])
- normalized.setdefault(n, []).append(s)
-
- for n, group in normalized.items():
- if len(group) > 1:
- print(f"Likely duplicates ({len(group)}):")
- for s in group:
- print(f" {s['pattern']} - {s['file']}:{s['line']}")
- ```
+ Normalize each pattern (lowercase, drop articles, collapse
+ parameters) and group by the normalized form; any group of size
+ greater than one is a candidate cluster. The overlap script that
+ does this is in
+ [references/step-extraction-and-overlap.md](references/step-extraction-and-overlap.md).
## Step 3 - Recommend consolidation
For each duplicate group:
```markdown
**Duplicate group:** 4 step definitions with normalized "user is
logged in"
| Pattern | File / line |
|------------------------------------|----------------------------|
| `Given a user is logged in` | `auth_steps.py:12` |
| `Given the user is logged in` | `cart_steps.py:8` |
| `Given an authenticated user` | `checkout_steps.py:5` |
| `Given a logged-in user` | `profile_steps.py:14` |
**Recommendation:** Consolidate to **`Given a logged-in user`**
(highest count of usage in current Gherkin; clearest wording).
Replace the other 3 step definitions with a single canonical one
in `shared_steps.py`. Update the Gherkin features that use the
deprecated patterns.
```
## Step 4 - Domain organization
Group steps per domain area:
```
features/steps/
├── shared/ # cross-domain (login, navigation, etc.)
│ ├── auth_steps.py
│ ├── navigation_steps.py
│ └── data_steps.py
├── checkout/ # checkout-specific
│ ├── promo_steps.py
│ ├── payment_steps.py
│ └── cart_steps.py
├── account/ # account-specific
│ └── profile_steps.py
└── README.md # step library index
```
The README is the discoverability artifact:
```markdown
# Step library
## Shared (cross-domain)
### Auth
- `Given a logged-in user` (auth_steps.py:12) - creates and logs in a generic test user.
- `Given a logged-in admin` (auth_steps.py:25) - logged-in user with admin role.
- `Given an unauthenticated visitor` (auth_steps.py:38) - no session.
### Navigation
- `When I navigate to {path}` - go to URL.
- `When I click {label}` - click any element with this label.
## Checkout
### Cart
- `Given the cart contains {qty} of {sku} at ${price}` - seed a cart.
- `Given the cart is empty` - empty cart.
### Promo codes
- `Given promo code {code} is active` - pre-seed a promo in the admin.
- `When I enter {code} in the promo input` - type the code.
- `When I click {label}` - (uses shared step).
(...)
```
The README + grep is the team's "is there a step for X?" tool.
## Step 5 - Pre-merge step gate
- Add a CI check that flags new step definitions:
-
- ```bash
- # scripts/check-new-steps.sh
- NEW_STEPS=$(git diff --diff-filter=A origin/main...HEAD -- '**/steps/*.py' '**/Steps/*.cs' '**/steps/*.java' \
- | grep -E '^\+' | grep -E '@(Given|When|Then)' | wc -l)
-
- if [ $NEW_STEPS -gt 0 ]; then
- echo "::warning::This PR adds $NEW_STEPS new step definitions."
- echo "Before merging, verify these aren't duplicates of existing steps."
- echo "Run: bash scripts/step-overlap.py"
- fi
- ```
-
- The check warns; doesn't block. Forces the author to acknowledge
- the new step.
+ Add a CI check that flags new step definitions so no engineer adds a
+ duplicate unknowingly. The check warns (doesn't block), forcing the
+ author to acknowledge the new step and grep the README first. The
+ `check-new-steps.sh` script is in
+ [references/step-extraction-and-overlap.md](references/step-extraction-and-overlap.md).
## Step 6 - Quarterly cadence
| Cadence | Trigger |
|--------------------|---------------------------------------------------|
| Quarterly | Scheduled step library review. |
| New team member | Onboarding: walk the README. |
| Step count exceeds threshold | Triggered review. |
| New domain area | Add steps; update README. |
+ ## Worked example
+
+ Curating a 142-step library in a mixed Behave project, end to end.
+
+ **1. Inventory.** Running the Behave extraction over `features/steps/`
+ yields 142 step definitions and 138 unique patterns - so 4 are
+ already ambiguous duplicates the runner would flag at match time:
+
+ ```
+ Total step definitions: 142
+ Unique patterns: 138
+ Given: 58 When: 34 Then: 47 And: 3
+ ```
+
+ **2. Detect.** The overlap script normalizes patterns and surfaces
+ one cluster:
+
+ ```
+ Likely duplicates (4): normalized "user is logged in"
+ Given a user is logged in - auth_steps.py:12
+ Given the user is logged in - cart_steps.py:8
+ Given an authenticated user - checkout_steps.py:5
+ Given a logged-in user - profile_steps.py:14
+ ```
+
+ **3. Consolidate.** Pick `Given a logged-in user` as canonical
+ (clearest wording, most-used in current Gherkin). Move it into
+ `features/steps/shared/auth_steps.py`, delete the other three
+ definitions, and rewrite the `.feature` files that used the
+ deprecated phrasings - edit the Gherkin first so no scenario is
+ left orphaned.
+
+ **4. Reorganize + publish.** Split the flat `features/steps/` into
+ `shared/`, `checkout/`, and `account/`, then regenerate the README
+ index. The library now advertises
+ `Given a logged-in user (auth_steps.py) - creates and logs in a generic test user`
+ under Shared → Auth.
+
+ **5. Gate.** Add the pre-merge new-step check. The next PR that adds
+ `Given the customer is signed in` trips the warning; the author greps
+ the README, finds `Given a logged-in user`, and reuses it instead of
+ adding a fifth variant.
+
+ Result: 142 → 139 step definitions, zero ambiguous duplicates, one
+ discoverable index the whole team searches before writing a step.
+
## Anti-patterns
| Anti-pattern | Why it fails | Fix |
|-----------------------------------------------------------------------|---------------------------------------------------------------------------|-----|
| Per-engineer step files (`alice_steps.py`) | Ownership without domain alignment; duplication across files. | Domain-organized files (Step 4). |
| Step library README missing | Discoverability nil; engineers re-implement. | README per Step 4. |
| Ambiguous step deletion without grep | Breaks scenarios silently. | Replace canonical → deprecated; update all Gherkin first. |
| Step "shared library" that's a giant `helpers.py` | All engineers conflict on it; merge hell. | Domain split (Step 4). |
| Reviewing step count quarterly only | Proliferation outpaces review; library bloats. | Pre-merge gate (Step 5). |
## Limitations
- **Heuristic duplicate detection.** Some patterns are
legitimately distinct (`Given a user` for auth vs `Given a user
with profile data` for profile tests).
- **Domain organization is per-team.** What's "shared" varies.
- **Consolidation cost.** Each refactor touches Gherkin + step
definitions; not free.
- **Doesn't fix Gherkin quality.** A clean step library doesn't
prevent imperative-style Gherkin scenarios. Pair with a Gherkin
style review.
## References
+ - Step extraction commands, the overlap-detection script, and the
+ pre-merge gate:
+ [references/step-extraction-and-overlap.md](references/step-extraction-and-overlap.md).
- `cucumber-testing`,
`behave-testing`,
`reqnroll-testing` - per-language
runners this curator works alongside.