Step Definitions, Hooks & Best Practices
Mapping Gherkin to Code
const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('assert');
Given('my cart contains {int} items', async function (count) {
this.cart = await addItemsToCart(count);
});
When('I proceed to checkout', async function () {
this.result = await checkout(this.cart);
});
Then('I should see an order confirmation', function () {
assert.strictEqual(this.result.status, 'confirmed');
});Cucumber Expressions ({int}, {string}) capture dynamic values from step text as arguments — a simpler alternative to raw regex for common patterns. Step definitions live separately from feature files: readable spec vs. executable logic.
Hooks
const { Before, After } = require('@cucumber/cucumber');
Before(async function () {
await resetTestDatabase();
});
After({ tags: '@ui' }, async function () {
await this.browser.close();
});Before/After are Cucumber's equivalent of beforeEach()/afterEach(), optionally scoped by tag. For UI tests, step definitions typically call into a separate browser automation library (Selenium, Playwright) — Cucumber itself has no browser automation of its own.
Living Documentation
Because feature files are actually executed as tests, they can't silently drift from real behavior the way a manually-maintained spec doc can — a failing test signals the doc and the code need reconciling.
Keeping Scenarios Business-Focused
Avoid writing steps around UI implementation details ("click the button with CSS selector .btn-submit") — prefer intent-focused language ("When I submit the form"). Implementation-heavy steps are both less readable to non-technical stakeholders and more brittle to unrelated UI changes. Reserve Gherkin for business-critical acceptance scenarios where cross-role readability adds real value — granular unit tests are often simpler written as plain code.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free