ScreenshotNeo

BlogHow-to

How to Fix Missing Step Implementations in the Second Feature File

Fix undefined steps in a second Cucumber or Behave feature by checking discovery, exact text, arguments, duplicates, and shared step organization.

By the ScreenshotNeo team30 September 20269 min read

How to Fix Missing Step Implementations in the Second Feature File

When steps work in one feature file but appear as undefined in a second file, the second file usually is not supposed to have its own step-definition registry. Cucumber loads step definitions before it runs scenarios, then matches each step’s complete text against all registered expressions. Behave follows the same practical model by importing Python files from the feature tree’s steps directory.

Start with discovery: make sure the runner’s glue package or Behave steps directory includes the implementation used by the second feature. Then compare the complete step text, captured arguments, data tables, and doc strings. Finally, remove duplicate or overlapping definitions. The feature filename does not determine which definition is available.

What “undefined” means

These failure states point to different fixes:

Both feature files use the same registry; discovery and unique matching determine the result.
Both feature files use the same registry; discovery and unique matching determine the result.
Message or state Meaning First action
Undefined No loaded definition matches the step text, or the definition was not discovered. Check glue/steps configuration and exact wording.
Ambiguous or duplicate More than one loaded definition matches. Delete the duplicate or narrow one expression.
Arity mismatch The expression captures a different number of arguments than the method accepts. Compare capture groups, parameters, tables, and doc strings.
Failed The definition matched and ran, but its code raised an error or an assertion failed. Debug the implementation and its test data.

Given, When, and Then are keywords for readability. They do not create separate matching namespaces. Matching is based on the step text after the keyword and on the registered expression.

Use one shared registry, organized by capability

You do not need one step-definition file for every feature. Cucumber’s organization guidance permits one or multiple files and recommends meaningful grouping that avoids duplication. Keep reusable login, catalog, payment, or navigation behavior together by capability. A second feature can reuse those definitions without copying them.

A practical layout for Cucumber-JVM might look like this:

src/test/resources/features/
  checkout.feature
  account.feature
src/test/java/com/example/bdd/steps/
  AuthenticationSteps.java
  CheckoutSteps.java
  AccountSteps.java
src/test/java/com/example/bdd/
  RunCucumberTest.java

For Behave, the feature tree normally contains the implementation directory:

features/
  account.feature
  checkout.feature
  steps/
    authentication_steps.py
    checkout_steps.py
  environment.py

Feature-coupled files such as checkout_feature_steps.py and account_feature_steps.py are not automatically wrong, but copying the same login step into both creates ambiguity and maintenance problems. Group by business capability and keep a single reusable definition where possible. See Cucumber’s step organization guidance and anti-patterns guide.

Diagnostic sequence for Cucumber-JVM

1. Confirm the runner and glue path

By default, Cucumber-JVM searches the runner class’s package and subpackages. If definitions are elsewhere, set an explicit glue package. Put the feature path, implementation package, and runner setting side by side while diagnosing.

package com.example.bdd;

import io.cucumber.junit.platform.engine.Cucumber;

@Cucumber
public class RunCucumberTest {
    // Cucumber discovers glue below the test package by default.
}

If your steps live outside that package, configure the runner through your build or platform configuration. For a JUnit 4 runner, the equivalent is commonly:

@RunWith(Cucumber.class)
@CucumberOptions(
    features = "classpath:features",
    glue = "com.example.bdd.steps",
    plugin = {"pretty"}
)
public class RunCucumberTest {}

Use the package that contains the step classes, not merely the package containing the feature files. The Cucumber FAQ identifies an incorrect glue path as a common reason an apparently implemented step remains undefined: Cucumber FAQ.

2. Run only the second feature with the same configuration

Run the second feature by its path or tag while preserving the same runner and glue options used for the first. If the message changes from undefined to failed, discovery and matching are fixed; debug the implementation next. Then run the complete suite to reveal duplicate matches or shared-state problems.

# Maven example
mvn test -Dcucumber.features=src/test/resources/features/account.feature

# Gradle example
./gradlew test --tests com.example.bdd.RunCucumberTest

3. Compare the complete step text

Compare the text after Given, When, or Then character by character. These are different steps:

When I log in as "alice"
When I sign in as "alice"
When I log in as "alice" with a password

A Cucumber expression can make the intended parameter explicit:

package com.example.bdd.steps;

import io.cucumber.java.en.When;

public class AuthenticationSteps {
    @When("I log in as {string}")
    public void logInAs(String username) {
        // call the test client's login operation
    }

    @When("I log in as {string} with a password")
    public void logInWithPassword(String username) {
        // use the scenario's password fixture
    }
}

Either change the second feature’s wording to match the existing expression or add a deliberately distinct expression. Do not rely on a similar meaning: matching is textual and parameter-aware.

4. Check arguments, tables, and doc strings

Every captured expression parameter must map to a method argument. A data table or doc string is an additional argument in Cucumber. For example:

When I create a user with:
  | name  | email          |
  | Alice | a@example.test |
@When("I create a user with:")
public void createUser(io.cucumber.datatable.DataTable table) {
    Map<String, String> user = table.asMaps().get(0);
    // create the user from user.get("name") and user.get("email")
}

If the method accepts no table, or accepts two strings while the expression captures one, Cucumber reports an arity problem rather than a normal assertion failure. Check optional text, capture groups, and converted types as well.

5. Search every loaded definition for duplicates

Because definitions are loaded before scenarios execute, a duplicate in a newly added file can affect the first feature too. Search for the literal wording and for broad expressions such as .* or a catch-all parameter. Remove the redundant method or narrow its expression so exactly one definition matches each step.

Diagnostic sequence for Behave

1. Put implementations under the expected steps directory

Behave imports Python files in the feature’s steps directory before execution. Confirm that the second feature is under the feature tree you actually run and that the implementation file is inside that tree:

Captured parameters, tables, and doc strings must match the implementation's arguments.
Captured parameters, tables, and doc strings must match the implementation's arguments.
# features/steps/authentication_steps.py
from behave import when

@when('I log in as "{username}"')
def step_log_in(context, username):
    context.client.login(username)

The matching feature text is:

Feature: Account access
  Scenario: Existing user signs in
    When I log in as "alice"

Check spelling, quotation marks, punctuation, and parameter placement. The Behave API describes decorator matching against the feature-step string: Behave API.

2. Run the second feature directly

behave features/account.feature --no-capture

If Behave cannot import the module, fix the Python import error first. If it reports undefined, inspect the decorator text and directory. If it reports a failed step, the decorator matched and the problem is inside the function.

3. Avoid duplicate decorators

Two decorators with the same or overlapping text can make a step ambiguous. Keep one implementation and call shared helper functions from capability-specific steps. Do not create a second copy only because a second feature was added.

Before-and-after example

Suppose checkout.feature already has:

When I log in as "alice"
@When("I log in as {string}")
public void logIn(String username) { /* ... */ }

The new file uses:

When I sign in as "alice"

That text has no match. The smallest fix is to make the feature wording identical. If the new scenario needs an additional value, update both the expression and method together:

When I log in as "alice" with password "correct-horse"
@When("I log in as {string} with password {string}")
public void logIn(String username, String password) { /* ... */ }

Prefer one canonical sentence when the behavior is the same. Synonyms multiply definitions and make future failures harder to diagnose.

Configuration checklist

  • Feature files are under the configured feature path.
  • The second feature is included by the command, tag filter, or build task.
  • Cucumber-JVM glue names the package containing step classes.
  • Behave implementation files are in the active feature tree’s steps directory.
  • Step text, punctuation, quotes, and parameters match exactly.
  • Method or function arity matches expression captures and table/doc-string arguments.
  • Only one loaded definition matches each step.
  • Shared setup is in hooks or reusable helpers, not copied into every feature.
  • The focused second-feature run and then the full suite use the same configuration.

Common errors and fixes

Symptom Likely cause Fix
First feature passes; second is undefined Second file is outside the selected feature path, or glue/steps discovery is wrong. Print the paths used by the runner and move the file or configure discovery explicitly.
Generated snippet looks implemented but remains undefined The snippet’s expression does not exactly match the feature text. Copy the complete step text into a Cucumber expression and add typed parameters.
Undefined after renaming a step Only the feature or only the definition was renamed. Rename both sides, including punctuation and quoted values.
Arity mismatch Capture groups or table/doc-string arguments differ from the method signature. Count every capture and trailing argument.
Ambiguous step Two files contain the same or overlapping expression. Delete the duplicate or narrow one expression.
Step now fails with a null or state error Matching works, but setup or scenario state is missing. Inspect hooks, fixtures, context objects, and scenario isolation.
Works in an IDE, fails in CI Different working directory, classpath, package, or feature filter. Log the resolved feature and glue paths and reproduce the CI command locally.

Performance, reliability, and maintenance

Step discovery happens before execution, so a large set of small files is normally less important than predictable configuration and unique expressions. Broad regular expressions increase matching work and ambiguity risk; specific Cucumber expressions are easier to review. Keep browser, HTTP, and database clients behind helper classes so steps remain short and deterministic.

For reliable suites, isolate scenario state, reset external data in hooks, and avoid ordering dependencies between feature files. A passing first feature does not prove that the second feature receives the same fixtures. Run the focused feature for fast diagnosis, then the full suite after changing shared definitions. Cache only immutable test data; shared mutable context can make an undefined-step fix appear intermittent when the actual problem is state leakage.

There is no per-feature step-definition cost. The maintenance cost comes from duplication: every copied login step can diverge in wording, setup, or cleanup. A capability-based registry gives later features a stable vocabulary.

Or skip the browser setup

If your next task is documenting the result of a tested page rather than debugging Cucumber, ScreenshotNeo can capture the page with one request. It accepts and removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. A direct call is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every plan includes features such as full-page and element capture, device presets, custom CSS and JavaScript, waits, headers, cookies, blocking rules, PDF output, caching, signed links, asynchronous jobs, bulk capture, and a usage API. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Do I need a new step file for every feature?

No. Reuse the shared registry. Add a file when a business capability needs clearer organization, not because the feature count increased.

Can the same sentence be used with Given and Then?

Keywords are not separate matching namespaces. Reusing the same text can match the same definition, although the sentence should still describe the action or assertion accurately.

Why does an undefined step show a suggested implementation?

The suggestion is generated from the unmatched text. It does not prove that the generated expression is in the loaded glue package or that its parameters match your method.

Should I use regex or Cucumber expressions?

Both can work. Cucumber expressions are usually easier to read and maintain; whichever you choose, keep expressions specific and verify capture arity.

What should I check first in CI?

Compare the exact feature path, glue or steps path, working directory, classpath, and tag filters between CI and the local command. A configuration difference can hide an otherwise correct definition.

Where are the official matching rules documented?

Use the Cucumber API documentation, the Cucumber FAQ, and Behave’s feature setup documentation.