skip to content

In cucumber-js, what locates your step definitions, and what does setWorldConstructor change?

level: middleimportance: must knowfreq 58%

answer

  1. Two discovery settings, not one
  2. Loading the module is the registration
  3. A fresh object for each scenario
  4. Arrow functions lose the binding
  5. setWorldConstructor and worldParameters

basics

~20 s

cucumber-js loads support code from the paths in its import or require options, defaulting to files beside the feature files; loading a module is what registers its steps. setWorldConstructor replaces the per-scenario World, which steps reach through this.

solid answer

~40 s

cucumber-js discovers two things through **separate settings**. Feature files come from `paths` (positional arguments or the `paths` key in `cucumber.js`/`cucumber.json`/`cucumber.yaml`), defaulting to the `features` directory. Support code comes from `import` for ES modules or `require` for CommonJS, with matching `--import` and `--require` options; the default sweeps files under the same directory tree as the features. Nothing is scanned by annotation — registration is a side effect of loading a module and calling `Given`/`When`/`Then` from `@cucumber/cucumber` at top level, so an unloaded file simply yields `Undefined` steps and a generated snippet. `setWorldConstructor(MyWorld)` replaces the object cucumber-js builds fresh for **every scenario** and binds as `this` in step and hook bodies. That is why a step written as an arrow function cannot see it: arrow functions keep their lexical `this`.

code

javascript · 18 lines
javascript
// features/support/world.js
const { setWorldConstructor, World } = require('@cucumber/cucumber')

class FerryWorld extends World {
  constructor(options) {
    super(options)
    this.port = options.parameters.port   // from worldParameters
    this.sailings = []
  }
}
setWorldConstructor(FerryWorld)

// features/step_definitions/timetable_steps.js
const { Given } = require('@cucumber/cucumber')

Given('the {word} route has {int} sailings', function (route, count) {
  this.sailings = new Array(count).fill(route)   // `this` is the FerryWorld
})

go deeper

for a junior

Be ready to say where cucumber-js looks for .feature files and for step definitions, and to name the package the Given/When/Then functions come from. Knowing that a fresh World exists per scenario is enough at this level.

for a middle

An interviewer expects the mechanics: which option locates features versus support code, that registration happens when a module is loaded rather than by annotation, and exactly why an arrow-function step cannot see the World.

for a senior

Show the operating judgement — keeping the World cheap, moving expensive setup into BeforeAll, feeding configuration through worldParameters, and recognising module-level state as the bug that only appears once parallel workers are enabled.

for a principal

Own the convention: what belongs on the World versus in a helper module, how profiles express environments, and how you stop the World growing into a god object that every step mutates and no one can reason about.

`cucumber-js` is the JavaScript and TypeScript member of the Cucumber family, published as the `@cucumber/cucumber` package. Two things surprise people arriving from Cucumber-JVM: nothing is found by annotation scanning, and the object your steps share is an ordinary class you can replace rather than a container you configure. ## How the runner finds things Feature files and support code are located by **different settings**, and confusing the two is the most common first-day failure. | Setting | What it locates | Command-line form | | --- | --- | --- | | `paths` | `.feature` files | positional arguments | | `require` | CommonJS support code | `--require` | | `import` | ES-module support code | `--import` | | `worldParameters` | data handed to the World constructor | `--world-parameters` | | `format` | which formatter writes output | `--format` | Those keys live in a configuration file — `cucumber.js`, `cucumber.cjs`, `cucumber.mjs`, `cucumber.json` or `cucumber.yaml` — and a named bundle of them is a **profile**, selected with `--profile`. One feature set can therefore have two run recipes (a fast tagged pass and a full nightly pass) without a second copy of anything. With no support-code option given, cucumber-js loads files that sit under the same directory tree as the features. That default is why the conventional layout puts `features/step_definitions` and `features/support` beside the `.feature` files: it is not a magic package name, it is simply where the default glob already looks. Move the glue elsewhere and you must say so with `import` or `require`. The registration model matters more than the paths. There is no attribute, decorator or annotation on a step function. Calling `Given('...', fn)` at module top level pushes an entry into a registry, so **loading the module is the registration**. The practical consequences: - A support file nobody loads contributes nothing, and the symptom is not a startup error but an `Undefined` step result with a generated snippet in the output. - Registration order is module-load order, which is why `setWorldConstructor` and custom parameter types belong in support files that load before, or independently of, the steps that rely on them. - Hooks are registered the same way: `Before`, `After`, `BeforeStep`, `AfterStep`, `BeforeAll` and `AfterAll` are functions you call, not names the runner looks up. ## What a custom World changes The **World** is the object cucumber-js constructs once per scenario and binds as `this` inside every step and scenario hook of that scenario. `setWorldConstructor(FerryWorld)` swaps the default for your class. Five things follow from that, and interviewers probe all of them: 1. **Regular functions only.** `Given('...', async function () { this.sailings })` works; the same body as `async () => { ... }` gets the enclosing module's `this` and silently sees `undefined`. This is the single most reported cucumber-js confusion. 2. **State is per scenario by construction.** Anything hung off `this` dies with the scenario. Module-level `let` variables survive it, which reads as a passing suite locally and as cross-talk once `--parallel` splits the run across worker processes. 3. **The constructor receives an options object** carrying `attach`, `log` and `parameters`. `attach` puts a screenshot or payload into the report, `log` writes a text note, and `parameters` is whatever JSON you supplied through `worldParameters` or `--world-parameters` — the clean way to inject a base URL or an environment name without a global. 4. **TypeScript needs the `this` type declared.** The idiom is a first parameter written as `function (this: FerryWorld, count: number)`, which the compiler erases; without it the custom members are invisible to the type checker. 5. **Construction cost is paid per scenario.** A World whose constructor opens a browser session or a database connection pays that for every scenario, so expensive shared setup moves into `BeforeAll` — which runs once in each worker process — while the World stays a cheap value holder. ## A worked case A ferry-timetable booking product has a 63-scenario feature set and an eleven-minute pipeline budget. The first cut put an HTTP client and a seeded timetable in the World constructor; wall time was 9m48s because 63 clients were built and torn down. Moving the client into `BeforeAll` and leaving only `this.bookingRef` and `this.sailings` on the World took it to 3m12s, and turning on four parallel workers took it to 1m05s — which only worked because no step reached for a module-level variable. ## Where this sits in the family The World is cucumber-js's answer to a question every implementation answers differently: *what object carries one scenario's state?* Behave answers `context`, passed as an explicit first argument to every step function. SpecFlow and Reqnroll answer with per-scenario instances resolved from a container into binding-class constructors. Cucumber-JVM answers with injected objects. The concept is shared; the spelling, the lifetime controls and the failure modes are not, and that is exactly the layer this question lives on.

  • Why does writing a cucumber-js step as an arrow function break access to a custom World?
    An arrow function has no `this` of its own; it captures the lexical `this` of the module that defined it. cucumber-js invokes the step body with the World as `this`, so the binding is simply ignored and the custom members read as `undefined`. Regular `function () { ... }` bodies are required, and TypeScript users additionally declare a `this: MyWorld` first parameter so the compiler sees the members.
  • How do you get an environment-specific base URL into a cucumber-js World without a global?
    Put it in `worldParameters` in the configuration file, or pass JSON on the command line with `--world-parameters`. cucumber-js hands that object to the World constructor as `options.parameters`, so the World reads it once and every step gets it through `this`. Profiles make this cleaner still: one profile per environment, each with its own `worldParameters` block.
  • What is the lifetime of the World relative to hooks and to parallel workers?
    One World per scenario. Scenario-level `Before` and `After` hooks share that scenario's World, so a hook can seed it and an `After` hook can read what the steps left. `BeforeAll` and `AfterAll` run outside any World. Under `--parallel` each worker is a separate process building its own Worlds, so nothing is shared across workers.

The World is a locker issued at the start of each scenario and emptied at the end; an arrow function is a step that walks past the locker room without picking up its key.

saying these in an interview costs you the question

  • Thinks cucumber-js scans the whole project for step definitions automatically
  • Writes steps as arrow functions and then reaches for module-level variables
  • Believes one World instance is shared by the entire run
  • Confuses the feature-file paths setting with the support-code setting
  • Opens a browser or database connection in the World constructor by default