Cucumber done right: BDD that talks to domain experts
Cucumber is either a bridge to your domain experts or an expensive layer of indirection — and which one depends entirely on how you write it. Here is where it earns its keep, where it doesn't, and the rules we follow.
Few tools in the Ruby world are as divisive as Cucumber. To its advocates it is the thing that finally lets non-programmers read, and even write, the acceptance criteria that drive development. To its detractors — and there are loud ones — it is a needless translation layer that turns a five-line test into a feature file, a regex, and a step definition, all to test what a plain Capybara spec could test directly.
Both camps are right, and that is the whole point. Cucumber is a power tool with a narrow purpose, and almost all the disappointment with it comes from using it for the wrong job. Here is how to tell the two apart, and the rules we follow to keep it on the right side.
What Cucumber actually is
Cucumber executes plain-language specifications. You write scenarios in Gherkin — a
small structured-English grammar of Given/When/Then — and back each line with
a Ruby step definition that performs the action or assertion:
Feature: Refunds
As a customer
I want to return an item within 30 days
So that I get my money back
Scenario: A refund within the return window
Given I bought a "Wool Scarf" 10 days ago
When I request a refund for the "Wool Scarf"
Then I should be refunded £25.00
And the item should be marked as returned
Given("I bought a {string} {int} days ago") do |product, days|
@order = create_order(product: product, placed_at: days.days.ago)
end
When("I request a refund for the {string}") do |product|
RefundService.new(@order).refund!(product)
end
Then("I should be refunded {float}") do |amount|
expect(@order.refunds.last.amount).to eq(amount)
end
The feature file is the specification; the step definitions are the glue between that English and your system. That separation is Cucumber’s entire reason to exist — and also the source of every complaint about it.
The dividing line: who reads the feature files?
Here is the single question that decides whether Cucumber is worth it: does a non-programmer actually read or write these scenarios?
If a product owner, a domain expert, a QA analyst, or a client genuinely opens the feature files — to agree the behaviour before you build it, to check it after, or to add a scenario themselves — then Cucumber is doing something nothing else can. The Gherkin is a shared artefact, a contract written in the language of the business that happens to be executable. That is enormously valuable on a project with real domain complexity and stakeholders who care about exact rules: refund windows, insurance eligibility, pricing tiers, regulatory constraints.
If, on the other hand, the only people who ever read the feature files are the developers who wrote the step definitions, then Cucumber is pure overhead. You have taken a test, scattered it across two files, and inserted a regex-matching layer in the middle — all the cost of the abstraction and none of its benefit. In that case a plain RSpec/Capybara feature spec says the same thing in one file with no translation tax, and you should use one. We do, often. Cucumber is not a default; it is a deliberate choice for a specific situation.
Rules for Cucumber that stays readable
When the situation does call for Cucumber, a handful of disciplines keep it on the valuable side of the line. Almost every “Cucumber is terrible” story is really a story about breaking these.
Write scenarios in the language of the domain, never the UI. This is the big one. The moment your steps talk about clicking buttons and filling fields, the business value evaporates and maintenance becomes a nightmare.
# BAD — this is a UI script, not a specification
When I fill in "email" with "a@b.com"
And I press "Submit"
And I click the third row of the table
# GOOD — this is behaviour a domain expert can read and will still be true
# after you redesign the page
When I sign in as a returning customer
And I request a refund for my most recent order
Domain-language steps survive UI redesigns, read like the requirement they encode, and can be implemented either through the UI or directly against the domain — which also makes them faster.
Keep step definitions thin. A step should delegate to a helper, a service object, or a page object — not contain a wall of logic. Logic in steps cannot be reused or tested and quietly turns your test suite into a second, worse application.
Reuse steps; do not breed them. A sprawling, near-duplicate step library
(I am logged in, I have logged in, I sign in) is how the dreaded
“undefined/ambiguous step” churn starts. Curate the vocabulary like the small
domain language it is meant to be.
Use Background and tables to stay DRY. A Background runs shared setup before
each scenario; a data table drives one scenario over many cases. Both keep features
declarative instead of repetitive.
Let the layers do their jobs. Cucumber is for end-to-end acceptance behaviour that a stakeholder cares about. It is the wrong tool for exhaustive edge cases, validation matrices, and algorithmic detail — those belong in fast unit specs. A healthy project has a few high-value Cucumber scenarios over a broad base of RSpec unit tests, not hundreds of slow browser scenarios trying to cover every branch. Inverting that pyramid is how teams end up with a twenty-minute suite they resent.
The honest cost
Even done well, Cucumber has a price. The indirection is real: a failing scenario means hopping from feature to step definition to helper to find the cause. The step-matching can surprise you with ambiguity. And full end-to-end scenarios driven through a browser are slow, so you must be ruthless about how many you keep. None of this is disqualifying — but it is why “is anyone outside the dev team reading these?” has to be a real yes, not an aspirational one.
Our verdict
Cucumber is a collaboration tool that happens to run tests, not a testing tool that happens to read nicely. On a project with genuine domain complexity and stakeholders who engage with the rules, writing those rules in executable Gherkin is worth every bit of the overhead — it catches misunderstandings before they become code and keeps a living, verified specification of how the business actually works. On a project without those stakeholders, it is ceremony, and a plain feature spec is the honest choice. Pick the tool for the situation, write your steps in the language of the domain rather than the DOM, and Cucumber goes from divisive to genuinely useful.