A Cucumber doc string carrying a YAML payload broke after the feature file was reindented - how does Cucumber decide which leading whitespace it strips?
answer
- The opening delimiter is the ruler
- Dedent is measured, not inferred
- Relative indentation is what survives
- A content type may follow the delimiter
- Parse the payload, do not diff strings
basics
~20 sCucumber removes from each doc-string line up to as many leading whitespace characters as the opening triple-quote delimiter is indented by. Relative indentation survives, so moving the body relative to the delimiter silently changes a whitespace-sensitive payload.
solid answer
~50 sA doc string opens and closes with a line containing only `"""` and reaches the step definition as its final argument. The dedent rule is measured **from the opening delimiter**: each content line loses up to as many leading whitespace characters as that delimiter is indented by, so nesting inside the block survives and a line indented less simply loses what it has. It is not "strip everything", and not "strip the common prefix of the content". That is why moving the body relative to the delimiter changes the string handed over, while moving the whole block together changes nothing. JSON shrugs that off; YAML, Markdown and raw string comparison do not. The durable fixes: parse the payload and assert on structure, and declare a content type after the opening delimiter so a corrupted payload fails loudly.
code
gherkin · 9 linesScenario: The kitchen receives the frozen headcount
When the 09:47 cut-off passes
Then the kitchen manifest is:
"""yaml
date: 2026-05-14
meals:
vegetarian: 118
standard: 197
"""go deeper
Know what a doc string is: a multi-line text block delimited by triple quotes under a step, handed to the step definition as one argument. Knowing that its indentation is meaningful is enough at this level.
Explain the dedent rule precisely - up to the opening delimiter's indentation is removed from each line - and contrast it with the two wrong models, stripping everything and stripping the common prefix of the content lines.
Demonstrate the diagnosis. A whitespace-only commit breaks an assertion that compares strings; show how you confirm it is whitespace, compare the delimiter's column across revisions, and move the suite to parsing the payload instead of diffing text.
Own the standard: whether large fixtures belong in feature files at all, whether formatters are allowed to touch the feature directory, and how a suite shared by two teams avoids a class of failure whose diff looks like pure formatting.
## How Cucumber delimits a doc string A doc string is a multi-line text argument attached to the step directly above it. It opens with a line containing only `"""` (triple backticks are accepted as an alternative delimiter) and closes with a matching line. The text between the delimiters is passed to the step definition as its final argument — a `String` in Cucumber-JVM, the last argument of the step function in cucumber-js, `context.text` in Behave, a string parameter in SpecFlow/Reqnroll. Only one doc string may be attached to a step, and it must be the last thing on that step, since it runs to its closing delimiter. To include a literal triple-quote inside the content, escape each quote with a backslash; Gherkin unescapes it before handing the text over. ## The dedent rule: the opening delimiter is the ruler Here is the mechanic the question turns on. **The indentation of the opening delimiter decides how much leading whitespace is removed from every content line.** Concretely: from each line of the content, Cucumber removes *up to* as many leading whitespace characters as the opening `"""` is indented by. A line indented less than the delimiter simply loses whatever leading whitespace it has; no visible character is ever eaten. ```gherkin Then the kitchen manifest is: """yaml date: 2026-05-14 meals: vegetarian: 118 """ ``` The delimiter sits at column 2, so two spaces come off each line. `date:` arrives at column 0 and `vegetarian:` at column 2 — the payload keeps its **relative** shape, which is exactly what makes an indented block readable in the feature file and still correct as YAML. Three corollaries follow, and each is a common wrong answer: - It is **not** "strip all leading whitespace" — that would flatten every nested structure. - It is **not** "strip the common prefix of the content lines" — the least-indented content line has no say; only the delimiter does. - The **closing** delimiter's indentation does not enter the calculation, and trailing whitespace on content lines is preserved. ## Why a reindent breaks a whitespace-sensitive payload | Payload | Sensitive to a shifted body? | What you see when it shifts | |---|---|---| | JSON | no | nothing; whitespace between tokens is insignificant | | YAML | yes | nesting changes, keys move under the wrong parent, or a parse error | | Markdown | yes | four extra spaces silently turn a paragraph into a code block | | Plain text compared as a string | yes | an assertion diff of invisible characters | The dangerous edit is any change that moves the **body relative to the delimiter** — reindenting the content lines while the delimiter stays put, moving the delimiter while the body stays put, or a formatter that normalises indentation inside the block. Move the whole block together and nothing changes, which is why the break feels random: an edit that looks like pure formatting alters the argument the step receives. On a school-meal ordering service whose suite two teams both edit, this shows up as a manifest scenario that passed for weeks and fails after a reformatting commit that touched nothing else. The diff is whitespace-only, the step definition is untouched, and the assertion compares two strings that look identical in the terminal. ## Diagnosing it, and stopping it recurring 1. **Confirm it is whitespace.** Print the received text with a visible marker per line, or compare lengths. A diff tool that ignores whitespace will hide exactly the thing that broke. 2. **Measure the delimiter.** Check the column of the opening `"""` against the column of the first content line in both the old and the new revision. That single comparison identifies the change. 3. **Stop comparing raw strings.** Parse the doc string in the step definition and assert on the resulting structure. A YAML or JSON payload compared as a string turns every cosmetic edit into a test failure; parsed and compared as data, indentation stops mattering. 4. **Use the content type.** A word may follow the opening delimiter — `"""yaml`, `"""json`. It records what the content is, and Cucumber-JVM can convert the doc string into a typed object by registering a `@DocStringType` for that content type. The payoff for this problem is that a corrupted payload then fails while being converted, with a parse message, instead of failing as an opaque string mismatch three assertions later. 5. **Keep bulk fixtures out of feature files.** A feature file is specification a non-engineer reads. A forty-line payload is not that; reference it by an identifier in the step and load it from a fixture file. This also shrinks the surface a formatter can disturb. 6. **Pin the formatter.** If an editor or repository-wide formatter touches `.feature` files, either configure it to leave them alone or agree on it across both teams — a suite where half the contributors reformat on save will keep producing this failure.
- How do you put a literal triple-quote inside a Cucumber doc string?Escape it: write the quotes with backslashes inside the content, and Gherkin unescapes them before handing the text to the step definition. Without the escape the parser reads the sequence as the closing delimiter and the rest of the payload becomes unparseable content in step position.
- The payload has grown to sixty lines of YAML. Should it stay in the feature file?Usually not. A feature file is specification that a non-engineer reads, and a sixty-line fixture drowns the behaviour it is meant to illustrate. Reference the fixture by an identifier in the step and load it from a data file. That also shrinks the surface a formatter can disturb, which removes this failure mode entirely.
- How does the doc string reach the step definition in each Cucumber implementation?As the step's final argument: a `String` parameter in Cucumber-JVM, which can be converted to a typed object by registering a `@DocStringType` for the declared content type; the last argument of the step function in cucumber-js; `context.text` in Behave; and a string parameter in SpecFlow/Reqnroll.
The opening triple quote is the margin ruler on a page: slide the ruler or slide the text and every line's offset changes, but move the whole block together and the layout is untouched.
saying these in an interview costs you the question
- Thinks all leading whitespace is stripped from the content
- Believes the dedent uses the least-indented content line
- Says the closing delimiter sets the indentation removed
- Claims the content type validates the payload by itself
- Compares whitespace-sensitive payloads as raw strings