Data-driven scenarios + tag filtering + parallel runs
One scenario, many rows of input. We turn the login scenario into a
Scenario Outline + Examples table, talk about type coercion in
Cucumber expressions, tag scenarios for selective runs, and finish by running multiple feature
files in parallel workers. Target stays the same: the TTACart sandbox.
01Scenario Outline + Examples
A Scenario Outline is a template. You write the steps once with
<placeholder> tokens, then add an Examples: table where each row
substitutes values into the placeholders. Cucumber expands the outline into one concrete
scenario per row at runtime.
features/login.feature
Feature: TTACart login - multi user
Background:
Given I am on the TTACart login page
Scenario Outline: <user> lands on <outcome>
When I sign in as "<user>" with password "<password>"
Then the outcome should be "<outcome>"
Examples:
| user | password | outcome |
| standard_user | tta_secret | products |
| locked_out_user | tta_secret | rejected |
| problem_user | tta_secret | products |
| performance_glitch_user| tta_secret | products |
How Cucumber expands the outline
flowchart LR
OUT[Scenario Outline + 4 rows] --> R1[Row 1: standard_user / products]
OUT --> R2[Row 2: locked_out_user / rejected]
OUT --> R3[Row 3: problem_user / products]
OUT --> R4[Row 4: performance_glitch_user / products]
R1 --> RUN[Cucumber runs 4 separate scenarios]
R2 --> RUN
R3 --> RUN
R4 --> RUN
RUN --> RPT[HTML report - 4 rows]
One Outline + four Examples rows = four concrete scenarios.Hand-drawn Examples grid - each row becomes its own concrete scenario.
Read the report. The HTML report shows one row per Examples line with the
substituted values in the scenario name. If row 2 is red, you immediately know
locked_out_user was the failing combination.
02Type coercion - string vs int
Examples cells are always raw text in the feature file. The Cucumber expression in the step
definition decides what type they become. {string} stays a string;
{int} becomes a number; {float} becomes a number with decimals.
Pick the one that matches what the step needs.
Step expression
Examples cell
Argument in handler
Pitfall
{string}
"3"
"3" (string)
Math fails
{int}
3
3 (number)
"3.5" won't match
{float}
3.5
3.5 (number)
Locale separator (, vs .)
{word}
standard_user
"standard_user"
Spaces break it
step-definitions/cart.steps.ts
// Outline row: | sku | qty | | sauce-labs-bike-light | 2 |
When('I add {string} to the cart {int} times', async function (this: CustomWorld, sku: string, qty: number) {
for (let i = 0; i < qty; i++) {
await this.inventoryPage.addToCart(sku);
}
});
Then('the cart badge should read {int}', async function (this: CustomWorld, expected: number) {
await expect(this.page.getByTestId('shopping-cart-badge')).toHaveText(String(expected));
});
The classic bug. Step says I add {string} to the cart {string} times
and you write for (let i = 0; i < qty; i++). JavaScript compares
"3" as a string - the loop runs 50 times (or zero, or fine, depending on the
other operand). Use {int} on numbers.
flowchart TB
CELL["Examples cell: '3'"] --> EXPR{Expression in step}
EXPR -->|{string}| S["'3' string"]
EXPR -->|{int}| I[3 number]
EXPR -->|{float}| F[3.0 number]
S --> BUG[Loop runs 0 or 50 times]
I --> OK[Loop runs 3 times]
F --> OK
Type coercion - the same raw cell becomes three different runtime values.
03Tags - the test suite's filter
A tag is a single token starting with @, written on its own line above a
Feature:, Scenario:, or Scenario Outline: (or above an
Examples: row). Tags are how you slice the suite. A @smoke
tag picks the 5-minute fast lane; @regression picks the 1-hour deep dive;
@wip hides scenarios that are not finished.
@smoke@regression@checkout@search@wip@flaky
features/checkout.feature
@checkout
Feature: TTACart checkout
@smoke
Scenario: One-item checkout (happy path)
Given I have a logged-in cart with 1 item
When I complete the checkout form
Then the order should be confirmed
@regression @flaky
Scenario: Checkout with shipping address validation
Given I have a logged-in cart with 1 item
When I submit an invalid postcode "0000A"
Then I should see a postcode validation error
@wip
Scenario: Apply a coupon code
Given I have a logged-in cart with 1 item
When I apply the coupon "TTA10"
Then the total should drop by 10%
Inherit tags from the Feature level
A tag at the Feature: level applies to every scenario in the file - so the file
above runs as @checkout @smoke, @checkout @regression @flaky, and
@checkout @wip for the three scenarios respectively.
04Tag expressions
The --tags CLI flag takes a tag expression - a small boolean language with
and, or, not, and parentheses. The expression
evaluates to true or false on each scenario's tag set; true means "run".
Expression
Meaning
Use case
@smoke
Run anything tagged smoke
PR pipeline
not @wip
Everything except WIP
Default local run
@smoke and not @flaky
Stable smoke set
Pre-deploy gate
@checkout or @search
Just checkout + search
Front-end team's slice
(@smoke or @regression) and not @flaky
Full stable suite
Nightly
Terminal
# Smoke only
npx cucumber-js --tags "@smoke"
# Smoke without WIP
npx cucumber-js --tags "@smoke and not @wip"
# Anything except flaky
npx cucumber-js --tags "not @flaky"
# Checkout or search, but never WIP or flaky
npx cucumber-js --tags "(@checkout or @search) and not (@wip or @flaky)"
flowchart TB
S[Scenario tags: @smoke @checkout] --> A{@smoke?}
A -->|yes| B{not @flaky?}
A -->|no| SKIP1[Skip]
B -->|yes| RUN[Run]
B -->|no| SKIP2[Skip]
Tag expression evaluator - the boolean tree decides per scenario.The tag tree - branch in, decide out.
05Multi-feature parallel runs
Cucumber-JS's --parallel N flag spins up N worker processes. Each worker pulls
one scenario at a time off a shared queue, runs it, returns to the queue. The
scenario is the unit of parallelism - not the feature file.
Four workers pulling scenarios in parallel - each owns its own Playwright context.Parallel timeline - four workers, scenarios interleaved across time.
Shared state is poison. Anything you stash in a module-level variable will be
read by every worker concurrently. Either keep state on the World (per-scenario) or use
isolated test data per scenario.
06Products feature - sort + filter data-driven
The TTACart inventory page has a sort dropdown:
data-test="product-sort-container" with four options
(az, za, lohi, hilo). We exercise all four
in one outline.
features/products.feature
Feature: TTACart products sort
Background:
Given I am signed in to TTACart as "standard_user"
And I am on the products page
@regression
Scenario Outline: Sort by <option> orders products correctly
When I sort the products by "<option>"
Then the first product name should be "<firstName>"
And the first product price should be <firstPrice>
Examples:
| option | firstName | firstPrice |
| az | TTA Backpack | 29.99 |
| za | TTA Wireless Mouse | 49.99 |
| lohi | TTA Sticker Pack | 7.99 |
| hilo | TTA Mechanical Keyboard| 129.99 |
step-definitions/products.steps.ts
When('I sort the products by {string}', async function (this: CustomWorld, key: string) {
await this.inventoryPage.sortBy(key);
});
Then('the first product name should be {string}', async function (this: CustomWorld, name: string) {
await expect(this.inventoryPage.firstItemName()).toHaveText(name);
});
Then('the first product price should be {float}', async function (this: CustomWorld, price: number) {
const text = await this.inventoryPage.firstItemPrice().innerText();
expect(parseFloat(text.replace(/[^\d.]/g, ''))).toBeCloseTo(price, 2);
});
flowchart LR
EX[Examples row] -->|substitute| WHEN[When I sort by 'az']
EX -->|substitute| THEN1[Then first name = 'TTA Backpack']
EX -->|substitute| THEN2[Then first price = 29.99]
WHEN --> RUN[Run scenario]
THEN1 --> RUN
THEN2 --> RUN
RUN --> RPT[Report row: az - pass]
Each Examples row threads three different placeholders into the same step set.
07External data sources (JSON)
Examples tables live in the feature file. For larger data sets (50-500 rows), pull from JSON
and inject into the World in a Before hook. The feature stays readable, the bulk data lives
in a file you can edit without touching the spec.
import productData from '../data/products.json';
export class CustomWorld extends World {
page!: Page;
products = productData as Array<{ key: string; firstName: string; firstPrice: number }>;
findProductRow(key: string) {
const row = this.products.find((r) => r.key === key);
if (!row) throw new Error('Unknown sort key: ' + key);
return row;
}
}
sequenceDiagram
participant F as Feature
participant W as World
participant J as products.json
F->>W: When I sort by 'az'
W->>J: findProductRow('az')
J-->>W: { firstName, firstPrice }
W->>F: Assertions use row data
JSON-backed data flow - feature stays small, JSON carries the volume.
08Anti-patterns to avoid
Outline with one Examples row. If you have a single concrete case, write a
Scenario, not an Outline. The Outline adds noise.
50-row Examples table. Past 8-10 rows, move the data to JSON and keep the feature
readable.
Tag soup. Three tags per scenario is fine; eight tags means you're using tags to
categorise data, not tests. Use the file structure instead.
Assertions in Background. Background is for state setup. If you assert there and it
fails, the report blames the first scenario - confusing.
Step parameters that should be tags.I sign in @as_admin is a tag;
I sign in as "admin" is a parameter. Pick one.
Rule of thumb. If a non-engineer can't predict what changing a placeholder
value will do, the outline is too clever.
PDrill exercises - lecture 2
Eight drills - all against TTACart. Open the Solution tab only after a real attempt.
1Convert login.feature to an Outline
Take your lecture-1 login.feature with two scenarios and rewrite it as a
single Scenario Outline + 4-row Examples table covering
standard_user, locked_out_user, problem_user,
performance_glitch_user.
Hint
Add an outcome column ("products" or "rejected") and one assertion step that branches on it.
2Type-coerce the cart quantity
Write a step I add "<sku>" to the cart <qty> times using
{int}. Run with qty = 0, 1, 5. Verify the cart badge matches.
Hint
Loop qty times calling
inventoryPage.addToCart(sku). Cart badge has
data-test="shopping-cart-badge".
3Tag five scenarios
Tag each scenario in your suite with @smoke, @regression, or
@wip as appropriate. Run only smoke. Then run "regression but not wip".
Hint
npx cucumber-js --tags "@smoke" and
npx cucumber-js --tags "@regression and not @wip".
4Mark a flaky scenario
Add @flaky to one scenario. Configure your default npm script to skip
flaky in PR runs. Verify the count goes down by one.
Hint
"test": "cucumber-js --tags \"not @flaky\"" in package.json.
5Sort outline with all four options
Build the products sort Outline shown in section 6. Run it. Make one row deliberately
wrong, look at the HTML report, identify which row failed.
Hint
Locator for the first item name:
page.getByTestId('inventory-item-name').first().
6Parallel-run 4 workers
Set parallel: 4 in cucumber.cjs. Run the whole suite. Time
before vs. after. Verify each scenario got its own browser context.
Hint
Add a console.log in Before printing
process.pid. Different PIDs across runs = different workers.
7JSON-backed data
Move the 4-row Examples table to data/products.json and inject the
matching row via the World in the step. Feature should stay 4-6 lines.
Hint
import data from '../data/products.json'
works because resolveJsonModule is on by default in modern
tsconfig.json.
8Detect a tag mistake
Tag one scenario @somke (typo). Run the smoke filter. The scenario is
silently skipped. Add a Cucumber CLI flag that exposes unmatched tags. Fix the typo.
Hint
There is no built-in "warn on unused tag" flag - the
fix is a lint script that scans .feature files and flags tags not in your
allowed list. A 10-line shell script does it.
SReference snippets - TS / Java / Python
Two snippets per language: the Scenario Outline + tag-filtered run, and the InventoryPage POM
with sort + cart helpers.
features/products.feature
Feature: TTACart sort
Background:
Given I am signed in to TTACart as "standard_user"
And I am on the products page
@regression
Scenario Outline: Sort by <option>
When I sort the products by "<option>"
Then the first product name should be "<firstName>"
And the first product price should be <firstPrice>
Examples:
| option | firstName | firstPrice |
| az | TTA Backpack | 29.99 |
| za | TTA Wireless Mouse | 49.99 |
| lohi | TTA Sticker Pack | 7.99 |
| hilo | TTA Mechanical Keyboard| 129.99 |
Feature: TTACart sort
Background:
Given I am signed in to TTACart as "standard_user"
And I am on the products page
@regression
Scenario Outline: Sort by <option>
When I sort the products by "<option>"
Then the first product name should be "<firstName>"
And the first product price should be <firstPrice>
Examples:
| option | firstName | firstPrice |
| az | TTA Backpack | 29.99 |
| za | TTA Wireless Mouse | 49.99 |
| lohi | TTA Sticker Pack | 7.99 |
| hilo | TTA Mechanical Keyboard| 129.99 |
src/test/java/pages/InventoryPage.java
package pages;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.SelectOption;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
public class InventoryPage {
private final Page page;
private final Locator sortSelect, items;
public InventoryPage(Page page) {
this.page = page;
this.sortSelect = page.getByTestId("product-sort-container");
this.items = page.getByTestId("inventory-item");
}
public void sortBy(String key) {
sortSelect.selectOption(new SelectOption().setValue(key));
}
public Locator firstItemName() { return items.first().getByTestId("inventory-item-name"); }
public Locator firstItemPrice() { return items.first().getByTestId("inventory-item-price"); }
}
features/products.feature
Feature: TTACart sort
Background:
Given I am signed in to TTACart as "standard_user"
And I am on the products page
@regression
Scenario Outline: Sort by <option>
When I sort the products by "<option>"
Then the first product name should be "<firstName>"
And the first product price should be <firstPrice>
Examples:
| option | firstName | firstPrice |
| az | TTA Backpack | 29.99 |
| za | TTA Wireless Mouse | 49.99 |
| lohi | TTA Sticker Pack | 7.99 |
| hilo | TTA Mechanical Keyboard| 129.99 |