skip to content

State Sharing & Dependency Injection

How step definitions in different classes share scenario state - the World in cucumber-js, PicoContainer, Spring or Guice on the JVM - and why static globals break a parallel run. A staple question.

on this pageshow

explore

questions

4

In cucumber-js, what is the World object and how long does one World instance live?

level: juniorimportance: must knowfreq 62%

answer

  1. Where one scenario keeps its own state
  2. The object bound to this in steps
  3. Created fresh, discarded at scenario end
  4. setWorldConstructor registers your own class
  5. Arrow functions never receive it

basics

~20 s

The World is cucumber-js's per-scenario context object, reachable as this inside step definition and hook functions. Cucumber builds a fresh World before every scenario and throws it away afterwards, so one scenario's state never reaches the next.

solid answer

~50 s

cucumber-js constructs one **World** object per scenario and invokes every step definition function and hook of that scenario with it bound to `this`. That is how steps that live in different files hand data to each other without a module-level variable: one step writes `this.scheduledCase`, a later step reads it. The default World is the `World` class exported by `@cucumber/cucumber` and already carries `attach` for screenshots and payloads, `log` for text, and `parameters` for world parameters supplied by configuration. You replace it by calling `setWorldConstructor(MyWorld)` once from support code; extend the exported `World` and pass the constructor's options object to `super` if you want those helpers kept. Lifetime is exactly one scenario, and each expanded `Examples` row counts as its own scenario, so it gets its own World. Because `this` is bound at call time, an arrow-function step definition never sees it.

code

javascript · 22 lines
javascript
const { Given, Then, setWorldConstructor, World } = require('@cucumber/cucumber')
const assert = require('assert')

class CourtroomWorld extends World {
  constructor(options) {
    super(options)
    this.scheduledCase = null
    this.courtroom = null
  }
}

setWorldConstructor(CourtroomWorld)

Given('case {string} is booked into courtroom {word}', function (caseNumber, room) {
  this.scheduledCase = caseNumber
  this.courtroom = room
})

Then('the docket shows the case in that courtroom', function () {
  assert.ok(this.scheduledCase)
  assert.strictEqual(this.courtroom, '4B')
})

go deeper

for a junior

Be ready to say what this refers to inside a cucumber-js step definition, and that a new World is built before each scenario. Knowing that alone explains why data set in a Given is visible in a later Then.

for a middle

An interviewer expects the mechanics: registering a class with setWorldConstructor, passing the constructor options to super, world parameters arriving on this.parameters, and why an arrow function breaks the binding.

for a senior

Show judgement about what the World holds. Expensive resources built in its constructor are paid per scenario, and a World with forty flat properties becomes a maintenance problem. Explain how you keep it small and typed.

for a principal

Own the convention across teams: one typed World shape, configuration entering only through world parameters, and no module-level caches sneaking back in. Be able to argue where the boundary sits between the World and the automation layer beneath it.

## What the World is cucumber-js builds **one object per scenario** and calls it the *World*. Before the first step of a scenario runs, Cucumber constructs a World; it then invokes every step definition function and every hook function belonging to that scenario with that object bound to `this`. When the scenario ends — passed, failed or skipped — the object is dropped, and the next scenario gets a brand new one. That is the whole design: **the World is one scenario's private scratch space.** It exists so that step definitions living in separate files can hand data to each other without a module-level variable. In a courtroom-scheduling suite, a `Given` writes the case number it just put on the docket onto `this`, and a `Then` three files away reads it back. If you register nothing, Cucumber instantiates its built-in World class, which already carries three useful members: - `this.attach(data, mediaType)` — attaches a screenshot, a response body or a log line to the scenario's result so formatters can render it. - `this.log(text)` — a shortcut for attaching plain text. - `this.parameters` — *world parameters*, supplied by the `--world-parameters` command-line option or the `worldParameters` configuration key. This is how a base URL or a scheduling-API token reaches steps without being hardcoded in a step definition file. ## Registering a custom World Custom Worlds are registered once, from support code, with `setWorldConstructor`. It takes the class itself, not an instance — Cucumber calls `new` on it for you, per scenario. ```javascript const { setWorldConstructor, World } = require('@cucumber/cucumber') class CourtroomWorld extends World { constructor(options) { super(options) this.scheduledCase = null this.conflicts = [] } } setWorldConstructor(CourtroomWorld) ``` Two things matter here. First, the constructor receives an **options object** which carries `attach`, `log` and `parameters`; pass it to `super(options)` when you extend the exported `World`, or those helpers stop working. Second, `setWorldConstructor` is a *registration*, not a per-file declaration — calling it in one support file applies it to the whole run, and calling it twice with different classes means the last registration wins. ## Lifetime, precisely | Moment | What happens to the World | | --- | --- | | Support code is loaded | Nothing — only the constructor is registered | | Just before a scenario's first hook | A new instance is constructed | | Every step and hook of that scenario | The same instance, bound as `this` | | Each row of an `Examples` table | A separate scenario, therefore a separate instance | | Scenario ends (pass, fail or skip) | The instance is discarded | | A retried attempt of the same scenario | A fresh instance for each attempt | Three consequences follow from that table: 1. **Anything expensive in the constructor is paid once per scenario.** A World that opens a database connection makes a 300-scenario suite open 300 connections. Long-lived resources belong outside the World; the World holds a *reference* to them. 2. **You cannot use the World as a cache across scenarios.** That is deliberate — the isolation is the feature. If two scenarios must see the same value, that value is configuration, not scenario state. 3. **A `Scenario Outline` with nine rows produces nine Worlds**, so a row cannot observe what an earlier row did. ## `this`, and the arrow-function trap `this` inside a step definition is bound **at call time** by Cucumber. An arrow function does not accept a `this` binding; it captures the `this` of the surrounding scope, which in a module is not the World. So this fails: ```javascript Given('case {string} is on the docket', (caseNumber) => { this.scheduledCase = caseNumber // not the World }) ``` and the classical `function` form works: ```javascript Given('case {string} is on the docket', function (caseNumber) { this.scheduledCase = caseNumber }) ``` The symptom is usually a `TypeError` on a property of `undefined`, or a value that silently reads back as `undefined` in a later step. The same rule applies to hook callbacks. TypeScript users often reach for a typed World class precisely so the compiler catches the mistake. ## What belongs on it - **Yes:** identifiers created by a `Given` (the hearing id, the courtroom, the case number), the last HTTP response, handles to resources scoped to this scenario. - **No:** constants and configuration — those come in through world parameters; and caches meant to survive scenarios, which reintroduce the coupling the World removes. - **Careful:** the World turns into a god object on a large suite. Group state into small domain objects held as fields (`this.docket`, `this.session`) rather than a flat list of forty properties, and add behaviour to those objects instead of writing helper functions that take the World as an argument.

  • Why does an arrow-function step definition in cucumber-js lose access to the World?
    Cucumber invokes the step function with the World as its `this`. An arrow function ignores a supplied `this` and captures the enclosing lexical scope instead, so the binding never lands. You get `undefined` or a `TypeError` on the first property access. Use the `function () {}` form for step definitions and hooks, or take a typed World reference deliberately rather than relying on `this`.
  • How do you get configuration such as a base URL into the World without hardcoding it?
    Use world parameters: pass them with the `--world-parameters` option or set `worldParameters` in the cucumber-js configuration, and they arrive on the World's constructor options and on `this.parameters`. A custom World typically reads them once in its constructor and exposes named fields, so step definitions never touch raw configuration.
  • What happens to the World when a scenario fails halfway through?
    Nothing special: the remaining steps are skipped, the scenario's hooks still run against the same World, and the instance is then discarded like any other. The next scenario is constructed from scratch. That is why an `After` hook can still read what the failing steps stored — for example, to attach the last response for diagnosis.

The World is the clerk's desk for one hearing: everything relevant is placed on it while the hearing runs, and it is cleared completely before the next case is called.

saying these in an interview costs you the question

  • Says the World is shared by every scenario in the run
  • Writes arrow-function steps and expects this to work
  • Keeps scenario state in module-level variables instead
  • Thinks setWorldConstructor is called per feature file
  • Cannot say when the World is created or discarded
  • Expects each Examples row to reuse one World
open as a page

In Cucumber-JVM with PicoContainer, how do two step definition classes share one state object?

level: middleimportance: should knowfreq 57%

basics

~20 s

Both step definition classes declare the shared type as a constructor parameter. With cucumber-picocontainer present, PicoContainer builds the glue classes per scenario, creating one instance of that type and injecting the same reference into every class that asks.

open as a page

In a parallel Cucumber-JVM run, what goes wrong when step definitions keep scenario state in a static field?

level: seniorimportance: should knowfreq 54%

basics

~10 s

A static field is one slot per class for the whole JVM, so concurrent scenarios read and write the same value. Scenarios overwrite each other's data, and the failure passes when rerun alone.

open as a page

In Cucumber-JVM with Spring, what does @CucumberContextConfiguration mark and why may only one class carry it?

level: middleimportance: nice to knowfreq 36%

basics

~20 s

@CucumberContextConfiguration marks the single glue class that tells cucumber-spring how to bootstrap the Spring test context. Exactly one entry point is allowed, so a second annotated class fails the run at startup. Step definition classes then become injectable Spring beans.

open as a page