Lecture 2
BDD Lecture 2 . Outlines + Examples + tags + parallel

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.
Examples table | user | password | outcome | | standard_user | tta_secret | products | | locked_out_user | tta_secret | rejected | | problem_user | tta_secret | products | | performance_glitch | tta_secret | products | -> 4 scenarios -> 4 reports -> 4 screenshots -> share one outline body Adding a row never changes the steps.
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 expressionExamples cellArgument in handlerPitfall
{string}"3""3" (string)Math fails
{int}33 (number)"3.5" won't match
{float}3.53.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".

ExpressionMeaningUse case
@smokeRun anything tagged smokePR pipeline
not @wipEverything except WIPDefault local run
@smoke and not @flakyStable smoke setPre-deploy gate
@checkout or @searchJust checkout + searchFront-end team's slice
(@smoke or @regression) and not @flakyFull stable suiteNightly
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.
Tag tree (hand drawn) @smoke @checkout @login @flaky => STOP Run if root true, every leaf true, no @flaky
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.

cucumber.cjs (parallel)
module.exports = {
  default: {
    requireModule: ['ts-node/register'],
    require: ['support/**/*.ts', 'step-definitions/**/*.ts'],
    paths: ['features/**/*.feature'],
    format: ['progress-bar', 'json:reports/cucumber.json', 'html:reports/cucumber.html'],
    parallel: 4
  }
};
flowchart LR
  Q[Scenario queue] --> W1[Worker 1]
  Q --> W2[Worker 2]
  Q --> W3[Worker 3]
  Q --> W4[Worker 4]
  W1 --> B1[Browser ctx 1]
  W2 --> B2[Browser ctx 2]
  W3 --> B3[Browser ctx 3]
  W4 --> B4[Browser ctx 4]
  B1 --> RPT[Merged JSON report]
  B2 --> RPT
  B3 --> RPT
  B4 --> RPT
Four workers pulling scenarios in parallel - each owns its own Playwright context.
Parallel timeline (4 workers) W1: login cart checkout W2: search sort A-Z remove W3: menu open reset W4: logout sort Z-A cart=0 time =>
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.

data/products.json
[
  { "key": "az",   "firstName": "TTA Backpack",            "firstPrice": 29.99 },
  { "key": "za",   "firstName": "TTA Wireless Mouse",      "firstPrice": 49.99 },
  { "key": "lohi", "firstName": "TTA Sticker Pack",        "firstPrice":  7.99 },
  { "key": "hilo", "firstName": "TTA Mechanical Keyboard", "firstPrice": 129.99 }
]
support/world.ts (excerpt)
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.