ScreenshotNeo

BlogGuides

Cucumber Best Practices for Reliable Test Automation

Write Cucumber scenarios that describe observable behavior, run independently, and make failures easy to diagnose. Includes practical Gherkin and JavaScript examples.

By the ScreenshotNeo team4 October 20269 min read

Cucumber tests are most reliable when each scenario describes one observable behavior, establishes its own starting conditions, and asserts a result explicitly. Keep the feature readable as an executable specification; put interaction mechanics and reusable setup in step definitions and helpers. Treat BDD as a collaborative way to discover and agree on examples, not as a synonym for writing Given/When/Then scripts.

1. Start with behavior, not test steps

Behavior-Driven Development (BDD) is an iterative practice: collaborators discover concrete examples, formulate and agree on them in a human- and machine-readable form, then automate them against the system. Cucumber is a tool for expressing and executing those examples. Its documentation makes clear that BDD is more than using Cucumber. Cucumber’s BDD documentation

Feature files can serve as executable specifications, automated tests, and documentation of system behavior. Keep them under version control alongside the software. Write them for the people who need to understand the behavior; avoid turning them into a transcript of browser clicks, internal database operations, or test framework mechanics. Cucumber documentation and guides

2. Write one focused scenario per behavior

A scenario should have one clear purpose and a failure that points to a particular behavior. It should contain enough context to explain the example, but not so much detail that changeable implementation choices become part of the specification. Independent scenarios are easier to interpret and can run in any order or in parallel without relying on one another’s side effects. Cucumber’s scenario guidance

For example, describe the result the user cares about instead of prescribing the current UI choreography:

Feature: Password reset

  Scenario: A registered user requests a reset link
    Given a registered user exists
    When the user requests a password reset
    Then the user is told how to continue

The scenario does not say which button was clicked, which email provider was called, or which table row was inserted. Those details belong in implementation code unless they are themselves the behavior under specification. The outcome still needs to be observable: a step definition must verify that the user receives the expected confirmation or other defined result.

3. Use Given, When, Then for distinct purposes

  • Given establishes a known starting state or precondition.
  • When describes the event or action being exercised.
  • Then asserts an observable outcome.

Cucumber suggests three to five steps per example as a useful guide to keeping examples expressive. It is guidance, not a hard syntax limit. If a scenario grows much longer, check whether it is combining independent behaviors or exposing too much setup detail. Gherkin reference

Keep the Then step an assertion, not another action. For example, “Then the user sees a confirmation” should inspect a visible confirmation or a well-defined response, rather than click another control and leave the result unchecked.

4. Make every scenario reproducible and independent

Each scenario should arrange the state it needs and should be runnable alone. Do not make one scenario depend on a record created by an earlier scenario, a particular execution order, or cleanup that another scenario happens to perform. Reuse helper methods for repeated mechanics such as creating a user or logging in, while keeping each scenario’s data and state isolated. Cucumber’s scenario guidance

Put meaningful preconditions in the feature when they help readers understand the behavior. A Background can express setup shared by the scenarios in a feature; scenario steps can express setup specific to one example. Use hooks for lifecycle work that does not need to be visible in the business-facing description. Hidden setup in a Before hook can make a feature hard to understand if it encodes a meaningful precondition. Cucumber hooks and API documentation

5. Keep step definitions clear, unique, and assertive

Step definitions connect Gherkin text to code. Keep their matching expressions narrow enough that a step has one clear implementation. Cucumber ignores the Given/When/Then keyword when matching step text, so changing a keyword does not create a distinct definition. Duplicate or overlapping expressions can therefore cause ambiguous matches. Cucumber step definitions

Here is a small runnable-style Cucumber-JS example. It assumes the project has Cucumber-JS and an application-specific resetPassword helper available; adapt that helper and the assertion to the system being tested.

const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('node:assert/strict');

Given('a registered user exists', async function () {
  this.user = await this.users.create({ email: 'ada@example.test' });
});

When('the user requests a password reset', async function () {
  this.result = await this.passwordReset.request(this.user.email);
});

Then('the user is told how to continue', function () {
  assert.equal(this.result.status, 'accepted');
  assert.match(this.result.message, /check your email/i);
});

The this.users and this.passwordReset objects are application test helpers, not built-in Cucumber APIs. Initialize them in a support file or appropriate hook. Use explicit assertions: Cucumber considers a step successful if its implementation does not raise an error. Returning false or another falsy value does not by itself fail the step. Undefined, pending, or failed steps cause later steps in that scenario to be skipped, which is another reason to keep scenarios focused. Cucumber API documentation

Share helper functions and scenario context rather than calling one step from another or invoking a scenario from a scenario. Step text is the specification interface; helper code is the implementation reuse point. This keeps control flow visible and avoids coupling scenarios together.

6. Use Background, hooks, and tags for the right jobs

  • Background: readable context shared by scenarios in one feature.
  • Scenario steps: behavior-specific setup that belongs to a particular example.
  • Hooks: lifecycle work such as opening and closing resources or setup that is clearer outside the feature narrative.
  • Tags: organizing features and scenarios, selecting a subset to execute, and restricting hooks to matching scenarios.

Keep the tag vocabulary small and tied to execution needs, such as a meaningful suite grouping or a resource requirement. Hooks and tags are tools for lifecycle and selection; neither makes an unclear scenario clear. Cucumber hooks and tags

7. Account for implementation-specific parallel behavior

Parallel lifecycle details differ among Cucumber implementations and versions. In cucumber-js, BeforeAll and AfterAll run once per worker by default in parallel mode. For setup that must happen once for the entire run, cucumber-js provides coordinator hooks. This behavior is specific to cucumber-js; check the versioned documentation for the implementation you use. The coordinator hook feature was added in cucumber-js v13.2.0. cucumber-js parallel execution documentation

Use worker-local hooks for resources each worker needs independently, such as its own browser instance. Use coordinator-level setup only for genuinely run-wide work. Keep scenario data isolated even when setup is shared; a global database or shared account can still create collisions between workers.

8. A practical review checklist

  • Does the scenario test one behavior with one clear purpose?
  • Can it run alone, in any order, and without another scenario’s side effects?
  • Does it establish its own required state?
  • Does the wording describe behavior a user or system observer can recognize?
  • Could the implementation change without requiring a wording change?
  • Does each Then step assert an observable outcome?
  • Does each step expression resolve to one definition?
  • Do parallel workers avoid sharing mutable scenario state?
  • Are hooks handling lifecycle work rather than hiding important behavior?

This checklist follows from Cucumber’s guidance on scenarios, step definitions, assertions, and parallel hooks. It can expose design risks, but it does not guarantee a test suite will never be flaky. Scenario design, step definitions, cucumber-js parallel execution

9. Troubleshoot common Cucumber problems

Symptom Likely cause Fix
Ambiguous step definition Two expressions match the same step text. Given/When/Then keywords do not distinguish matching definitions. Narrow or consolidate expressions and make the intended step wording unambiguous.
A step returns false but passes A falsy return value is not an assertion failure signal. Use an assertion that throws on mismatch, or raise an error when the expected outcome is absent.
Later steps do not execute An earlier step is undefined, pending, or failed; Cucumber skips the rest of that scenario. Resolve the first reported step problem. If the scenario has unrelated outcomes, split it into focused scenarios.
A scenario passes only after another scenario It relies on shared state, execution order, or another scenario’s setup. Arrange its state independently and isolate its data and cleanup.
Parallel runs fail intermittently Workers may be contending over shared data or a resource initialized with the wrong lifecycle scope. Use worker-local resources where needed, isolate scenario data, and verify implementation-specific parallel hook semantics.
Feature files are hard to maintain Scenarios describe click sequences, internal details, or hidden preconditions instead of behavior. Move mechanics into helpers and step definitions; expose meaningful preconditions in Background or scenario steps.

10. Performance, reliability, and maintenance

Short, focused scenarios make failures easier to locate and reduce the amount of unrelated setup a failure can implicate. Independent setup also enables order-independent and parallel execution, provided the test environment and data are isolated. Parallelism itself does not fix shared-state bugs; it can reveal them. Keep setup proportional to the behavior under test and use helpers for repeated mechanics without hiding the scenario’s meaning.

There is no source-backed universal percentage for how much Cucumber practices improve reliability or speed. Treat the three-to-five-step recommendation as writing guidance, not a performance benchmark. Review failures and execution behavior in your own system rather than assuming a particular scenario length or hook arrangement guarantees speed.

11. Capture browser behavior with ScreenshotNeo

For scenarios where the evidence is a rendered page, a screenshot can help capture the visual state behind a failure. ScreenshotNeo is a website screenshot API and MCP server for developers, made by Yorker Media. It accepts a URL and returns a PNG, JPEG, WebP, or PDF. See ScreenshotNeo and the API documentation.

Or skip the browser setup:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Replace the example URL with the page you want to capture. The Python and Node.js examples save the returned response body; check response status and headers in production code so an API error is not mistaken for an image. See the ScreenshotNeo documentation for request options.

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. All features are on every plan.

Sign up for ScreenshotNeo to get 1,000 free screenshots a month with no card.

12. FAQ

How detailed should my scenarios be?

Detailed enough to communicate the behavior, initial context, action, and observable outcome. Keep mechanics and volatile implementation details out of business-facing wording. Three to five steps is Cucumber’s useful rule of thumb, not a strict limit.

How do I share state between steps?

Use the scenario-scoped context or state mechanism provided by your Cucumber implementation, and place reusable operations in helpers. Avoid process-wide mutable state for scenario data, especially when running in parallel.

How do I call other steps or scenarios?

Do not use one scenario as a subroutine. Extract shared behavior into a helper function or service and call it from the relevant step definitions. This keeps each scenario independently readable and runnable.

Should every test be written in Gherkin?

No. Gherkin is useful when an example benefits from being a shared, readable description of behavior. Implementation-focused checks can live in the test layers and formats that best express them.