Feature Testing Setup
Before writing many features, decide where files live, how they are discovered, and how the test code separates from application code. A deliberate layout prevents import problems, duplicated hooks, and CI friction later. This chapter establishes the standard behave project skeleton and the choices you should make once.
Standard Directory Structure
project/
behave.ini
features/
environment.py
steps/
authentication_steps.py
api_steps.py
features1.feature
features2.feature
src/ # application code under test
myapp/
__init__.py
service.py
tests/ # non-BDD unit tests, unrelated to behave
Key rules: step modules belong in features/steps/ so behave imports them automatically; environment.py must sit directly in the feature directory; helper modules shared by steps can live in features/steps/ or anywhere importable. If application code lives in src/, add that path to sys.path in environment.py or install the package in editable mode (pip install -e .).
Naming and Partitioning Features
- One
.featurefile per capability, e.g.,account_password_reset.feature. - Avoid catch-all files like
all_tests.feature— they grow unmanageably. - Import subdirectories under
features/are also scanned; both file and directory paths work (features/my/entrance/myfile.featureor the whole tree at once). - Keep step files themed like feature files so reviewers can locate implementations fast: all login steps in one module, all HTTP steps in another.
Minimal First Feature Set
Seed the setup with one feature and one steps module to prove paths and imports resolve:
Feature: Service health
Scenario: Endpoint answers
Given the service is running
When the health endpoint is polled
Then the status is OK
from behave import given, when, then
@given("the service is running")
def step_running(context):
context.base_url = "http://localhost:8080"
@when("the health endpoint is polled")
def step_poll(context):
import requests
context.response = requests.get(context.base_url + "/health")
@then("the status is OK")
def step_status(context):
assert context.response.status_code == 200
Configuration Side: behave.ini
Place behave.ini in the directory from which you run (project root is typical) so teammates and CI need no per-machine flags:
[behave]
paths = features
steps_dir = steps
stdout_capture = yes
stderr_capture = yes
format = pretty
default_tags = ~@skip
Command-line arguments always override behave.ini, so the file encodes common defaults, not absolutes.
Environment Side: environment.py
Use environment.py for cross-scenario infrastructure — clients, database connections, test data factories — not for scenario logic:
# features/environment.py
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
def before_all(context):
context.config.userdata.get("env", "staging") # -D env=... on the CLI
def before_scenario(context, scenario):
context.base_url = "http://localhost:8080"
def after_scenario(context, scenario):
# cleanup that must run even on failure goes here
pass
Running From the Right Directory
Behave resolves features relative to the current working directory. Run it from the project root in local shells, IDE launch configurations, and CI pipelines; otherwise paths in behave.ini silently miss the feature tree and behave exits with "no feature files found".
Summary
A sound setup is a features/ tree with a steps/ subfolder, an environment.py for hooks and imports, and a behave.ini for shared defaults. Keep application imports resolveable and always launch behave from the project root — these two decisions cause most new-team setup pain.