zio-bdd

A BDD testing framework integrating Gherkin-style tests with ZIO’s effect system

View the Project on GitHub EtaCassiopeia/zio-bdd

Gherkin Syntax Reference

zio-bdd implements a line-oriented Gherkin parser. This document is the complete syntax reference for every construct the parser accepts. If it is not listed here, it is not supported.


File structure

A feature file is a UTF-8 text file with the .feature extension. Every file contains exactly one Feature: block. The parser handles:

The general structure of a file from top to bottom is:

[feature-level tags]
Feature: <name>
  [description paragraph]
  [Background:]
  [Rule: blocks | Scenario blocks...]

Keywords

Feature

Feature: Payment Processing

Feature: is the only mandatory keyword. Everything else is optional. The feature name is the text on the same line after Feature:. It may be empty.

A description paragraph — free-form prose lines — may follow Feature: before the first Background:, Rule:, or Scenario:. Description text is silently ignored; it is not stored or validated.

A feature with no scenarios is valid. This is useful for documenting planned behaviour before writing step definitions.


Background

Background:
  Given the database is seeded
  And the audit log is empty

Background: precedes the scenarios in a feature and contains steps that are automatically prepended to every scenario’s step list. The background steps are indistinguishable from ordinary steps once expanded — they carry the same types (Given, When, Then, etc.) and can have data tables and doc strings.

Rule inner Background: A Rule: block may contain its own Background:. Inner background steps are appended after the feature-level background steps:

Feature: Account Lifecycle

  Background:
    Given the system is running         # prepended to every scenario

  Rule: EOD must be idempotent

    Background:
      Given a provisioned account       # prepended only to this rule's scenarios

    Scenario: EOD is idempotent
      When EOD runs twice
      Then the state is unchanged
      # step list: "the system is running", "a provisioned account", "When...", "Then..."

There is at most one Background: per feature and one per Rule: block.


Scenario

Scenario: Happy path login
  Given a valid user exists
  When the user submits credentials
  Then a session token is returned

Scenario: introduces a single test case. The name is the text after the colon on the same line. The name may be empty.

Alias: Example: is accepted as a synonym for Scenario:.

Example: Happy path login
  Given ...

Rule

Rule: Accounts must be active before posting

  Scenario: posting to an inactive account fails
    Given an inactive account
    When a post request is sent
    Then a 422 response is returned

Rule: groups related scenarios under a business-rule heading. The rule name is free text. A Rule: block may contain:

Rules may not be nested. The feature may contain any mix of top-level scenarios and Rule: blocks; however, mixing them in the same file is not spec-valid Gherkin, though the parser tolerates it.


Scenario Outline / Scenario Template

Scenario Outline: Post a <type> transaction
  Given a provisioned account
  When a <type> post of <amount> is sent
  Then the balance changes by <amount>

Scenario Outline: (alias: Scenario Template:) is a parameterised scenario. Angle- bracket placeholders — <columnName> — are substituted with values from the Examples: table. Each data row in the Examples: table produces one expanded Scenario.

The expanded scenario name is:

<outline name> - Example <N>

or, when the Examples: block has a name:

<outline name> - <examples name> - Example <N>

If a placeholder used in a step has no matching column header in the Examples: table the step is silently dropped from the expanded scenario — no step-not-found error or warning is raised.


Examples / Scenarios

Examples:
  | type       | amount |
  | deposit    | 100    |
  | withdrawal | 50     |

Examples: (alias: Scenarios:) follows a Scenario Outline: block. It is a data table where the first row is the header and every subsequent row produces one scenario expansion. Column names must match the <placeholder> names in the outline.

Named blocks: an Examples: block may have a name:

Examples: Happy path
  | user  |
  | Alice |

Multiple blocks: a single outline may have multiple Examples: blocks. Each block produces its own set of expanded scenarios independently.

Per-block tags: tags placed before an Examples: block are applied to all scenarios expanded from that block. They are merged with the tags from the enclosing Scenario Outline:.

@smoke
Scenario Outline: Validate <x>
  Given <x> is present

@regression
Examples: Block A
  | x |
  | a |

@slow
Examples: Block B
  | x |
  | b |

Scenarios from Block A carry smoke and regression. Scenarios from Block B carry smoke and slow.

@property(...) tag — generative Examples: when an Examples: block (or its parent Scenario Outline:) carries a @property(...) tag AND the block has no data rows (header only), the block triggers property-based testing instead of literal expansion. The framework samples values from the HasGen[T] registry for each column and runs the scenario N times.

@property(samples=500, seed=42)
Scenario Outline: Balance is never negative
  Given an account with balance <balance>
  When I withdraw <amount>
  Then the balance is not negative

  Examples:
    | balance | amount |

Column headers may carry a : generatorName suffix to select a named generator override for that column:

  Examples:
    | balance | amount: smallAmounts |

See Property-Based Testing for the full reference.


Steps

Steps are the executable lines inside a scenario. They begin with one of the five keywords:

Keyword Type recorded
Given GivenStep
When WhenStep
Then ThenStep
And AndStep
But ButStep

The wildcard bullet * is also accepted and records as AndStep:

* the system is running
* the database is seeded

Step text is the remainder of the line after the keyword and any whitespace. Text is stripped of leading and trailing whitespace.

Word-boundary enforcement: keywords are only recognised at the start of a stripped line, and only when followed by whitespace, a colon, or end of line. Text that starts with a keyword word is not mis-parsed:

Then Givenness matters   # → ThenStep, pattern "Givenness matters"
Then Andromeda galaxy    # → ThenStep, pattern "Andromeda galaxy"

Colon variant: Given: a step (colon immediately after keyword) is accepted.


Tags

Tags appear on a line by themselves, before the element they annotate. Multiple tags may appear on one line or across multiple consecutive lines — all are merged:

@smoke @regression
@priority-high
Scenario: Provision an account

Produces tags: smoke, regression, priority-high.

Placement:

Tag format: a tag is @ followed by one or more non-whitespace characters. Dashes, dots, underscores, parentheses, digits, and Unicode letters are all accepted:

@spec-1.4.7
@retry(3)
@flaky
@日本語

@ignore tag: when a scenario (or feature) has the ignore tag (case-insensitive), it is recorded as ignored and its step bodies are not executed.

@ignore
Scenario: Not yet implemented
  Given a step that does not exist yet

@flags(k=v) tag: a special built-in tag that triggers flag-matrix expansion. See testing-flags.md for the full reference.


Data tables

A data table follows a step on the next indented lines. The first row is the header; all subsequent rows are data rows.

Given the following users exist:
  | name  | role  | active |
  | Alice | admin | true   |
  | Bob   | user  | false  |

Cell handling:

Pipe escape: a literal | inside a cell is written as \|:

Given the regex pattern:
  | pattern  |
  | a\|b\|c  |
# cell value: a|b|c

Other escape sequences inside cell text:

Written Stored as
\| \||
\\ \\\
\n \n → newline character

@ColumnName annotation: when a step definition uses the @ColumnName annotation on a case-class field, the framework maps table columns to fields by name. See step-dsl.md for details.


Doc strings

A doc string attaches a multi-line text block to a step. Two delimiter styles are supported:

Triple-quote ("""):

Given the request body:
  """
  {
    "accountId": "abc-123",
    "amount": 1000
  }
  """
Then done

Triple-backtick ( `):

Given the SQL query:

SELECT * FROM accounts WHERE active = true

Then the result set is non-empty

Indentation stripping: the leading whitespace equal to the indentation of the opening delimiter is stripped from every content line. Relative indentation inside the block is preserved. Empty lines within the block are preserved as empty strings.

The doc string is stored in Step.docString: Option[String] and is accessible in step bodies.


Comments

Lines beginning with # (after optional leading whitespace) are stripped before parsing. Comments may appear anywhere — before Feature:, between steps, inside tables, between scenarios.

# This is a top-level comment
Feature: Payment Processing

  # Background comment
  Background:
    Given the system is running  # inline comments are NOT supported — this text
                                 # "the system is running  # inline..." is the step text

Inline comments are not supported. Only full-line comments (where # is the first non-whitespace character) are stripped. Text after a step keyword that contains # is part of the step text.


Complete example

# project-level comment
# language: en

@project @smoke
Feature: Account Lifecycle
  As a financial system I want to manage accounts.

  Background:
    Given the system is running
    And the following instrument classes exist:
      | Class         | Version |
      | SimpleSavings | 1.0.0   |

  @provision @smoke
  Scenario: Provision an account
    When a provision request is sent
    Then the ledger returns a 200 status code

  @post
  Scenario Outline: Post a <type> transaction
    Given a provisioned account
    When a <type> post of <amount> is sent
    Then the balance changes by <amount>

  Examples: Deposits
    | type    | amount |
    | deposit | 1000   |
    | deposit | 5000   |

  @smoke
  Examples: Withdrawals
    | type       | amount |
    | withdrawal | 500    |

  Rule: EOD must be idempotent

    Background:
      Given a provisioned account with a balance

    Scenario: EOD run twice is idempotent
      When EOD is run on 2025-01-01
      And EOD is run again on 2025-01-01
      Then the account state is unchanged

What is NOT supported

The following Gherkin features are intentionally absent. Attempting to use them will either be silently ignored or cause a parse failure.

Unsupported feature Behaviour
Multiple Feature: blocks in one file Parser stops at the second Feature: keyword; only the first feature is returned
Non-English keywords (Fonctionnalité:, Szene:, etc.) # language: directive is stripped but only English keywords are recognised
Inline comments (Given a step # comment) # inside a step line is part of the step text
@ColumnName derivation without annotation Plain case-class mapping requires explicit @ColumnName on fields
DocString content type hints (json ``) | The type hint after `` is captured as part of the delimiter but not stored separately  
Nested Rule: blocks Not parsed as nested; the inner Rule: is promoted to a sibling top-level rule, so its scenarios get only the feature’s Background:, not the outer rule’s
Multi-feature files Not supported; use separate .feature files
Ability: / Business Need: as aliases for Feature: Not recognised
Step argument ordering constraints The parser places a data table or doc string on the immediately following step; two arguments on one step are not supported