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 checkoutThe 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 passwordsTechnically 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 pageRules for Background:
- There can only be one per feature file
- It runs before
@Beforehooks 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 resultsIn 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 201The 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 passwordTags at the feature level apply to all scenarios in the file. Use tags to:
- Select which tests to run (
@smokefor 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 200Good:
When I submit the login form
Then I should be taken to my dashboardGood 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 reducedBetter: 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" buttonDeclarative (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 emptyGood:
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 listName 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 2Precise:
Scenario: Login succeeds with valid credentials
Scenario: Login fails when account is locked
Scenario: Login page shows CAPTCHA after 5 failed attemptsUse 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.