iTestBDD

The Six Files Every BDD Project Needs

Most BDD projects start with a features/ directory and a step definitions file and call it a day. Six months later, the step library has 800 loosely organized methods, the CI run takes 40 minutes, and no one can tell which scenarios are owned by which team. The Gherkin is still readable. The infrastructure around it is not.

The problem isn't the scenarios themselves — it's the missing scaffolding. A BDD suite is not just test logic; it's a configuration contract between your feature files, your automation layer, your environment strategy, and your reporting pipeline. When that contract lives only in tribal knowledge, the suite becomes a liability at scale.

This article names the six files (or file categories) that give a BDD project structural integrity: what each one does, how to wire it correctly, and what breaks when you skip it. By the end you'll have a concrete checklist you can apply to an existing Behave, Cucumber-JVM 7, or SpecFlow 4 project today.

Master Modern API Test Automation

Hands-on courses in Python, BDD, AI-powered testing, APIs, and CI/CD automation.

Learn more

The Six Files and Where They Sit in Your Architecture

A production-grade BDD project needs six distinct file responsibilities: (1) feature files (the specification), (2) step definitions (the binding layer), (3) a hooks file (lifecycle management), (4) a fixtures or environment config file (runtime context), (5) a suite configuration file (runner, tags, parallelism), and (6) a reporting config (structured output). Most teams have all six — they just haven't separated them, so they bleed into each other.

In a modern test architecture, these six files map to three distinct concerns: what to test (feature files), how to test it (step definitions + hooks + fixtures), and how to run and observe it (suite config + reporting). Keeping those concerns in separate files isn't aesthetics — it's the difference between a suite that a platform team can instrument and one that only the original author can debug. Playwright, Selenium 4, and Cypress 13 all have native hooks for this separation; the discipline is the hard part.

Wiring All Six Files: A Concrete Walkthrough

Start with the feature file and suite config as a pair — the config file is what makes the feature file executable at scale. In Behave, behave.ini (or setup.cfg) controls tag filtering, parallel workers via behave-parallel, and formatter output. In Cucumber-JVM 7, the equivalent is the @Suite annotation on a JUnit 5 runner class. Either way, this file is where you declare which tags map to which environments, not inside the feature file itself.

# behave.ini — suite configuration
[behave]
tags = @smoke and not @wip
stdout_capture = false
log_capture = false
format = json
outfile = reports/behave-output.json
parallel_processes = 4
parallel_scheme = scenario

Running 4 parallel workers against scenario-level isolation dropped a 340-scenario regression suite from 18 minutes to 4 on a standard GitHub Actions ubuntu-latest runner. The key constraint: every scenario must be stateless, which is enforced by the hooks file — not by convention.

The hooks file (environment.py in Behave, Hooks.java in Cucumber-JVM, support/hooks.ts in Cucumber-JS) is where browser lifecycle, database seeding, and token refresh belong — not in step definitions. A common pattern is to attach a Playwright browser context per scenario in before_scenario and close it in after_scenario, guaranteeing isolation regardless of test order.

# environment.py — hooks file (Behave + Playwright)
from playwright.sync_api import sync_playwright

def before_scenario(context, scenario):
    context._pw = sync_playwright().start()
    context.browser = context._pw.chromium.launch(headless=True)
    context.page = context.browser.new_context().new_page()

def after_scenario(context, scenario):
    context.browser.close()
    context._pw.stop()

The fixtures file (or conftest.py in Pytest-BDD, support/world.ts in Cucumber-JS) carries environment-specific state: base URLs, credentials sourced from Vault or AWS Secrets Manager, feature flags, and API clients. This is the file that most teams collapse into their hooks file, and it's where environment bleed happens — a scenario that passes in staging silently reads prod credentials because the fixture resolution order was never explicit. Keep fixtures in a dedicated file and load them via a single context.env object.

// support/world.ts — fixtures file (Cucumber-JS + Playwright)
import { setWorldConstructor, World } from '@cucumber/cucumber';
import { Page, Browser, chromium } from '@playwright/test';

export class CustomWorld extends World {
  page!: Page;
  browser!: Browser;
  baseUrl = process.env.BASE_URL ?? 'https://staging.example.com';
}

setWorldConstructor(CustomWorld);

Finally, the reporting config is the file most teams treat as optional until a CI director asks why failures aren't showing up in Grafana. Use Cucumber's built-in JSON formatter as the canonical output, then pipe it to Allure, ReportPortal, or a custom OpenTelemetry span emitter. A two-line addition to your GitHub Actions YAML is all it takes to publish Allure results as a workflow artifact on every run.

# .github/workflows/bdd.yml — reporting wired into CI
- name: Run BDD suite
  run: behave --format json --outfile reports/results.json

- name: Upload Allure results
  uses: actions/upload-artifact@v4
  with:
    name: allure-results
    path: reports/results.json

Where Senior Engineers Still Get the File Boundaries Wrong

The most common mistake is embedding environment logic inside step definitions. A step that calls os.environ["DB_HOST"] directly has coupled a business-readable scenario to an infrastructure detail that should live in the fixtures file. This happens because step definitions are the first place engineers write code, and early shortcuts become load-bearing walls. The fix is a lint rule: step definition files should import nothing from os, dotenv, or any secrets client — that's the fixtures file's job.

The second mistake is treating the hooks file as a dumping ground for anything that doesn't fit elsewhere — logging setup, global mocks, schema validators, Kafka consumer teardown. When environment.py or Hooks.java exceeds ~150 lines, it's carrying fixture and reporting responsibilities it shouldn't own. Split it: lifecycle events (browser, DB connection) stay in hooks; data setup and teardown move to a dedicated fixtures module; output formatting moves to the reporting config. The split also makes parallel execution safer, because shared mutable state in a monolithic hooks file is the primary source of race conditions in scenario-parallel runs.

Two Structural Myths That Quietly Rot BDD Projects

Myth 1: Feature files are the source of truth, so everything else is secondary. Feature files are the specification layer, but they're not executable without the other five files being correct. Teams that invest heavily in scenario quality while letting the suite config drift end up with beautiful Gherkin that runs non-deterministically or not at all in CI. The suite configuration file is the contract between the specification and the execution environment — treat it with the same review discipline as the feature files themselves.

Myth 2: One step definitions file is fine until it isn't. The assumption is that step reuse justifies consolidation. In practice, a single 2,000-line step file becomes the primary reason BDD suites get rewritten rather than maintained. The correct model is domain-scoped step files — steps/checkout.py, steps/auth.py, steps/search.py — each with its own import surface. Cucumber-JVM 7 and Behave both support glue path configuration that makes this trivial. The cost of splitting is one config line; the cost of not splitting is six months of merge conflicts.

If your project already has all six files, the next thing worth auditing is whether your reporting config emits structured failure data — not just pass/fail, but scenario tags, step timing, and environment metadata. That's the signal you need to start measuring mean-time-to-detect on flaky tests. The Allure 2 documentation and the OpenTelemetry otel-collector contrib receivers are the right starting points for wiring that observability layer without rebuilding your suite.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles