Gherkin Syntax Guide: Writing Good Feature Files

Gherkin Syntax Guide: Writing Good Feature Files

Gherkin is the plain-language format that makes BDD tests readable by non-technical stakeholders. It looks deceptively simple — just English sentences with a few keywords — but writing good Gherkin is a skill. Poorly written feature files create tests that are brittle, hard to maintain, and misleading to read.

This guide covers the full Gherkin syntax, the rules behind each keyword, and the patterns that separate clear, maintainable feature files from tangled walls of text.

What Is Gherkin?

Gherkin is a domain-specific language (DSL) used by Cucumber, Behave, SpecFlow, and most other BDD frameworks. A .feature file written in Gherkin describes application behaviour from the user's perspective, using structured natural language.

The key value: the same file serves as both the test specification and automated test driver. Stakeholders read it to understand what the system does; the framework executes it to verify the system does it.

The Complete Keyword Reference

Feature

Every .feature file starts with exactly one Feature block. It gives the file a name and optional description:

Feature: Shopping Cart
  In order to purchase items
  As a registered customer
  I want to manage my cart before checkout

The text after Feature: is the name. Lines below (before the first Scenario) are the description — parsed but not executed. Use the description to explain why this feature exists, not what the scenarios do.

Scenario

A Scenario describes one concrete example of how the feature works:

Scenario: Add a single item to an empty cart
  Given the cart is empty
  When I add "Wireless Headphones" to the cart
  Then the cart should contain 1 item
  And the total should be "$79.99"

Each scenario is independent — it should not depend on state left by a previous scenario. Scenarios run in the order they appear in the file, but you must never rely on that order for correctness.

Given / When / Then / And / But

These are the step keywords. They map to step definitions in your code.

Keyword Purpose
Given Sets up pre-conditions (the world's initial state)
When Describes the action the user takes
Then Describes the expected outcome
And Continues the previous step's type
But A contrasting continuation (often used with Then)
Given I am logged in as "alice@example.com"
And my account has a Pro subscription
When I click "Export Data"
Then a CSV file should be downloaded
But the file should not contain passwords

Technically And and But inherit the type of the step above them — Cucumber treats them identically to the preceding Given, When, or Then. The choice is purely for readability.

Background

Background runs before every scenario in the file. Use it for setup steps that every scenario shares:

Feature: Product Reviews

  Background:
    Given the database has been seeded with test products
    And I am logged in as a verified buyer

  Scenario: Submit a 5-star review
    When I navigate to the "Wireless Headphones" product page
    And I submit a review with 5 stars and text "Amazing sound quality"
    Then my review should appear at the top of the page

  Scenario: Submit a review with a photo
    When I navigate to the "Wireless Headphones" product page
    And I attach a photo to my review
    And I submit the review
    Then the photo should be visible on the product page

Rules for Background:

  • There can only be one per feature file
  • It runs before @Before hooks in some frameworks — check your framework's order
  • Keep it short; if it's more than 3-4 steps, the scenarios probably need to be split into separate files

Scenario Outline (and Examples)

Scenario Outline lets you run the same scenario with different data sets:

Scenario Outline: User registration with various email domains
  Given the registration form is open
  When I register with email "<email>" and password "Str0ngP@ss"
  Then I should receive a confirmation email to "<email>"
  And my account should be created with username "<expected_username>"

  Examples:
    | email                   | expected_username |
    | alice@gmail.com         | alice             |
    | bob.smith@company.org   | bob.smith         |
    | carol+tag@outlook.com   | carol+tag         |

The <placeholder> syntax in the scenario maps to column headers in the Examples table. Cucumber generates one scenario per row.

You can have multiple Examples blocks in one outline, and you can tag them individually:

Scenario Outline: Payment processing
  When I pay with "<method>"
  Then the order status should be "<status>"

  @fast
  Examples: Card payments
    | method       | status    |
    | Visa         | confirmed |
    | Mastercard   | confirmed |

  @slow
  Examples: Bank transfers
    | method          | status  |
    | Bank transfer   | pending |

Data Tables

When a step needs structured data (not just a single value), use a data table:

Given the following users exist in the system:
  | name    | email              | role  |
  | Alice   | alice@example.com  | admin |
  | Bob     | bob@example.com    | user  |
  | Carol   | carol@example.com  | user  |

When I search for users with role "user"
Then I should see 2 results

In your step definition, Cucumber passes the table as a DataTable object (Java), table fixture (Ruby), or similar. You can convert it to a list of maps, a raw list, or custom objects.

@Given("the following users exist in the system:")
public void createUsers(DataTable table) {
    List<Map<String, String>> rows = table.asMaps();
    for (Map<String, String> row : rows) {
        userService.create(row.get("name"), row.get("email"), row.get("role"));
    }
}

Doc Strings

For multi-line strings (JSON, XML, long text), use doc strings with triple quotes:

When I send a POST request to "/api/users" with body:
  """json
  {
    "name": "Alice",
    "email": "alice@example.com",
    "role": "admin"
  }
  """
Then the response status should be 201

The content type hint after the opening """ (e.g., json, xml) is optional — it's for documentation and syntax highlighting in editors, not parsed by Cucumber.

Tags

Tags are @annotations that appear above Feature, Scenario, or Scenario Outline:

@authentication @smoke
Feature: User Login

  @happy-path
  Scenario: Successful login

  @error-handling @regression
  Scenario: Login with wrong password

Tags at the feature level apply to all scenarios in the file. Use tags to:

  • Select which tests to run (@smoke for fast CI gates)
  • Mark work in progress (@wip)
  • Indicate test requirements (@requires-database)
  • Filter reporting
# Cucumber JVM
mvn <span class="hljs-built_in">test -Dcucumber.filter.tags=<span class="hljs-string">"@smoke and not @wip"

<span class="hljs-comment"># Behave
behave --tags smoke --tags ~wip

<span class="hljs-comment"># SpecFlow
dotnet <span class="hljs-built_in">test --filter <span class="hljs-string">"Category=smoke"

Rule (Cucumber 6+)

Rule groups related scenarios that all illustrate one business rule:

Feature: Discount Application

  Rule: Free shipping on orders over $50
    Scenario: Order of exactly $50 qualifies
      Given my cart total is "$50.00"
      When I view the checkout page
      Then shipping should show as "FREE"

    Scenario: Order below $50 does not qualify
      Given my cart total is "$49.99"
      When I view the checkout page
      Then shipping cost should be shown

  Rule: Loyalty members always get free shipping
    Scenario: Loyalty member with small order
      Given I am a loyalty program member
      And my cart total is "$10.00"
      When I view the checkout page
      Then shipping should show as "FREE"

Rule is optional but helps organise large feature files around distinct business rules.

Writing Good Gherkin: The Principles

Describe behaviour, not implementation

Bad:

When I click the button with id "submit-btn"
And the JavaScript function validateForm() returns true
Then the POST request to /api/login returns 200

Good:

When I submit the login form
Then I should be taken to my dashboard

Good Gherkin describes what the user experiences, not how the code achieves it. Implementation details belong in step definitions.

One scenario = one behaviour

Each scenario should test exactly one thing. If a scenario has 15 steps, it's probably testing multiple behaviours and will be hard to diagnose when it fails.

Too broad:

Scenario: Full checkout flow
  Given I am logged in
  When I add 3 items to cart
  And I apply a discount code
  And I enter my shipping address
  And I enter my credit card
  And I confirm the order
  Then I should receive an email
  And my order should appear in order history
  And inventory should be reduced

Better: split into Cart management, Checkout flow, Order confirmation, and Inventory update scenarios in separate feature files.

Use declarative language, not imperative

Imperative (avoid):

When I click the "Profile" link in the navigation
And I click the "Edit" button
And I clear the "Name" field
And I type "Alice Smith" in the "Name" field
And I click the "Save" button

Declarative (prefer):

When I update my profile name to "Alice Smith"

The step definition handles the implementation detail. The scenario reads as business intent.

Make scenarios self-contained

Don't assume another scenario ran first:

Bad:

Scenario: Delete the user created in the previous test
  When I click delete on the first user
  Then the user list should be empty

Good:

Scenario: Delete a user
  Given a user "alice@example.com" exists in the system
  When I delete the user "alice@example.com"
  Then "alice@example.com" should no longer appear in the user list

Name scenarios precisely

Scenario names appear in test reports. Vague names make failures hard to diagnose.

Too vague:

Scenario: Test login
Scenario: Error case
Scenario: Edge case 2

Precise:

Scenario: Login succeeds with valid credentials
Scenario: Login fails when account is locked
Scenario: Login page shows CAPTCHA after 5 failed attempts

Use Background sparingly

Background is only appropriate when every scenario in the file genuinely needs those steps. If only some scenarios need certain setup, use tagged hooks or move those scenarios to a different file.

Common Mistakes

Sharing state across scenarios — each scenario should set up its own state. If scenario B depends on scenario A having run first, you have an integration test masquerading as a scenario, and it will break whenever you run scenarios in isolation.

Implementation leak — step definitions that call driver.findElement(By.id("btnSubmit")) directly in the scenario text (When I click #btnSubmit) expose implementation details that break when HTML changes.

Scenario Outline overuse — outline tables are powerful but can obscure what's actually being tested. If the examples are testing fundamentally different behaviours (not just different data for the same behaviour), write separate named scenarios instead.

Skipping the description — the narrative section under Feature: and Scenario: descriptions are often left empty. Use them. They're read by product managers and new team members to understand context.

Tooling and Editor Support

Most IDEs have plugins for Gherkin syntax highlighting and step navigation:

  • IntelliJ IDEA / WebStorm — bundled Cucumber plugin, step navigation with Ctrl+Click
  • VS Code — "Cucumber (Gherkin) Full Support" extension
  • Eclipse — "Cucumber Eclipse Plugin"

Good tooling makes writing feature files faster and catches undefined steps before you run the tests.

Summary

Gherkin's value comes from discipline in how you write it. The keywords are simple; the craft is in choosing the right level of abstraction — enough detail to be testable, abstract enough to read like documentation.

Keep scenarios focused on one behaviour, write in the language of your business domain, and let the step definitions worry about the mechanics. The result is a living specification that teams can actually read, maintain, and trust.

Read more

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Atlantis automates Terraform plan and apply through pull requests. But Atlantis itself needs testing: workflow configuration, plan output validation, policy enforcement, and server health checks. This guide covers testing Atlantis workflows locally with atlantis-local, validating plan outputs with custom scripts, enforcing Terraform policies with OPA and Conftest, and monitoring Atlantis

By HelpMeTest