Which parts of a Cucumber-family suite actually port between cucumber-js, Behave and Reqnroll?
answer
- One artefact travels; the rest is rewritten
- Shared grammar, shared dialect data
- The step body calls your production code
- Patterns, tables and tag syntax all differ
- Sharing a specification, not a suite
basics
~20 sThe .feature file ports, because the Gherkin grammar and its dialect data are shared across the family. Almost nothing below it does: step definitions, hooks, the per-scenario state object, data-table access, tag-filter syntax, configuration files and report formats are all implementation-specific.
solid answer
~50 sOne artefact travels: the `.feature` file. The Gherkin parser and the dialect data that defines localised keywords are shared, so a file written for Cucumber-JVM parses under cucumber-js, Behave or Reqnroll, including its `# language:` header. Everything **below** the feature file is rewritten per implementation — the step-definition code and the way it is registered, the pattern syntax (Cucumber Expressions on some implementations, `parse` patterns or regular expressions on others), the hook mechanism, the per-scenario state object, the data-table access API, the configuration file, the tag-filter command-line grammar and the report formats. The practical consequence for a shared feature-file library is that you are sharing a **specification**, not a test suite: two glue layers must be kept in step with one file, an unimplemented step surfaces as `Undefined` on one side only, and reporting needs normalising before a single dashboard can show both.
code
gherkin · 14 lines# language: en
Feature: Sailing cancellation refunds
Rule: Cancellations inside 4 hours of departure are non-refundable
Scenario Outline: Refund due on a cancelled booking
Given a booking on the Harbour Point sailing at <departure>
When the passenger cancels at <cancelled_at>
Then the refund due is <refund> pence
Examples:
| departure | cancelled_at | refund |
| 07:20 | 22:10 | 4730 |
| 07:20 | 04:05 | 0 |go deeper
Know that the .feature file is the shared artefact and that step definitions are written per language. Being able to say which parts are plain Gherkin and which parts are code is enough here.
Explain the specific gaps: pattern syntax, data-table access, hook registration, state object and configuration file all differ, and the shared grammar only covers the feature file itself.
Argue from why — the step body calls your production code, so it cannot travel — and describe how you would run a shared feature-file package: undefined steps failing, both consumers in CI, and normalised reporting.
Own the contract: release cadence for the shared package, how step wording changes are deprecated, which scenarios belong in it at all, and whether the coordination cost is repaid by having one wording of each rule.
"Cucumber" names a family, not a program: Cucumber-JVM, cucumber-js, Behave and SpecFlow/Reqnroll are separate codebases that agree on a **language** and very little else. Knowing precisely where the agreement stops is what separates a workable shared-specification strategy from an expensive one. ## What genuinely ports The `.feature` file. The Gherkin grammar — `Feature`, `Rule`, `Background`, `Scenario`, `Scenario Outline` with its `Examples` table, the `Given`/`When`/`Then`/`And`/`But`/`*` step keywords, doc strings, data tables, comments and tags — is defined once and implemented against the same dialect data, which is also what makes the `# language:` header behave the same everywhere. A file authored against one implementation will normally parse under another. "Normally" is doing real work in that sentence. Support for newer Gherkin keywords lands in implementations at different times, so a file using a recently added keyword can parse in one and fail in another. If you intend to share files, pin your conventions to the constructs every target implementation demonstrably supports and prove it with a parse check in CI rather than assuming. ## What does not port, and why | Concern | Cucumber-JVM | cucumber-js | Behave | SpecFlow/Reqnroll | | --- | --- | --- | --- | --- | | Marking glue | annotated methods | functions called at module load | `@given`/`@when`/`@then` decorators | methods in a `[Binding]` class | | Pattern syntax | Cucumber Expressions or regex | Cucumber Expressions or regex | `parse` patterns or regex | regular expressions | | Per-scenario state | injected objects | the `World` bound to `this` | the `context` argument | constructor-injected classes | | Hooks | annotations | `Before`/`After` registered at load | named functions in `environment.py` | `[BeforeScenario]` and friends | | Feature binding | at run time | at run time | at run time | code generated at build time | The deepest reason the glue cannot travel is not syntax at all: **a step body calls your production code**. A step that books a sailing constructs your booking client, in your language, against your service's own types and test doubles. Rewriting that step in a second language does not port the test — it re-implements it, including the client, the fixtures and the assertions. The feature file is the only part with no dependency on the system under test. Several smaller gaps bite in practice: - **Pattern syntax.** Cucumber Expressions with `{int}`, `{string}` and `{word}` are not universal. Behave matches with its own `parse` patterns or regular expressions; the .NET implementations have historically used regular expressions. A step *phrase* ports; the pattern that captures its arguments usually does not. - **Data tables.** Every implementation gives you a table object and every one spells the accessors differently, and the typed-conversion helpers differ more still. - **Tag filtering.** The `@smoke and not @wip` tag-expression grammar is not the command-line syntax everywhere; Behave's `--tags` option has historically used its own comma-and-repetition form. A shared CI template that assumes one grammar will silently select the wrong scenarios. - **Configuration.** `cucumber.js`/`cucumber.json` profiles, `behave.ini`, .NET project configuration — three unrelated file formats with different key names. - **Reports.** The Cucumber Messages stream is not emitted by every member of the family, so a single dashboard across implementations means normalising output rather than concatenating it. ## What that means for a shared feature-file library Consider a ferry-timetable booking product whose 63-scenario feature set describes timetable, fare and cancellation rules, consumed by a Node booking API and a .NET back-office service under an eleven-minute pipeline budget. Publishing the `.feature` files as a versioned package is genuinely valuable — one wording of each rule, one review, one place a product change lands. But it comes with obligations: 1. **Two glue layers, one source of truth.** Every scenario needs an implementation on each side, and each side's step wording must match the shared file exactly. 2. **Drift is asymmetric and quiet.** Add a scenario to the shared package and one consumer reports it as `Undefined` while the other passes. Run both consumers against every published version in CI, and treat `Undefined` as a failure rather than a warning. 3. **Versioning is a contract.** Consumers must be able to lag by a version, so the package needs a release cadence and a deprecation story for changed step wording. 4. **Reporting needs normalising** before anyone can answer "is this rule covered?" across both suites. 5. **Only share what is genuinely shared.** Rules about fares and cancellations belong in the shared package; scenarios about one service's admin screens do not, and forcing them in is how a shared library turns into a coordination tax. The honest summary for an interview: the feature file is portable because it names behaviour; the glue is not portable because it names your code. Share the specification, expect to write the automation twice, and make CI prove that both halves still agree.
- You publish feature files as a package to a Node suite and a .NET suite. How do you stop them drifting?Make undefined steps fail rather than warn, and run both consumers against each published version in CI before the version is released. Version the package so consumers can lag deliberately rather than accidentally, and require a step-wording change to ship as a new version with both glue layers updated. Normalise both suites' reports into one coverage view so an unimplemented rule is visible, not merely absent.
- Which is more portable across the family: a step's wording or its pattern?The wording. The Gherkin line is plain text and reads identically everywhere. The pattern that captures its arguments is implementation-specific: Cucumber Expressions such as `{int}` and `{string}` are not universal, Behave matches with `parse` patterns or regular expressions, and the .NET implementations have historically used regular expressions. Teams sharing files therefore agree on phrasing conventions, not on pattern syntax.
- Why is a single cross-implementation report harder than it looks?The implementations do not all emit the same machine-readable output, so you cannot simply concatenate results. Building one view means normalising each suite's output into a common shape keyed by feature and scenario name — which then makes scenario names load-bearing identifiers, so renaming a scenario breaks history unless the shared package treats names as part of its contract.
The feature file is sheet music: any ensemble can read it, but the parts have to be arranged for the instruments each ensemble actually plays.
saying these in an interview costs you the question
- Assumes step definitions can be shared with a thin adapter layer
- Believes Cucumber Expressions work identically in every implementation
- Thinks one tag-filter command line works across all four runners
- Expects to concatenate reports from different implementations directly
- Treats an undefined step in one consumer as a warning rather than a failure