beehave is a simpler alternative to behave and pytest-bdd. Instead of writing step definitions that match Gherkin step text to Python functions with @given/@when/@then decorators, beehave links scenarios to tests by function name alone. It generates pure Hypothesis property-based test stubs from your .feature files and checks that your test code stays consistent with your spec. Your tests never import beehave — they just use hypothesis.
pip install beehaveTraditional BDD frameworks (behave, pytest-bdd) require step definitions — separate Python functions decorated with @given/@when/@then whose text must match the Gherkin step text exactly. This creates fragile coupling, boilerplate, and framework lock-in. beehave eliminates all of that:
- No step definitions. The function name is the link.
Scenario: guard bee inspects visitor→test_guard_bee_inspects_visitor. - No runtime imports. Your tests import only
hypothesis. beehave is a dev-time CLI. - Property-based by default. Hypothesis
@given()strategies are inferred from Examples table types. behave and pytest-bdd are example-only.
To make this work, beehave applies a few constraints beyond standard Gherkin:
| Constraint | Why |
|---|---|
| Titles contain only letters, digits, and spaces | They become Python identifiers (test_...) and file paths |
<placeholder> names must be valid Python identifiers, not keywords or builtins |
They become function parameters |
"quoted strings" and bare numbers in step text are enforced literals |
check verifies they appear as Constant nodes in the function body |
| Scenario titles are globally unique across all features | One function name = one scenario, everywhere |
# docs/features/hive_activity.feature
Feature: Hive Activity
Background:
Given the hive is active
Scenario Outline: honey production from nectar
Given the hive has <nectar> grams of nectar
And the evaporation rate is <rate> percent
When the bees fan their wings for <hours> hours
Then the hive produces <honey> grams of honey
Examples:
| nectar | rate | hours | honey |
| 100 | 20 | 8 | 80 |
| 200 | 25 | 12 | 150 |
| 50 | 30 | 6 | 35 |
Rule: Hive defense
Background:
Given the entrance has 2 guards
Scenario: guard bee inspects visitor
Given a visitor bee with <scent> colony odor
When the guard inspects the visitor for "floral" scent
Then the visitor is <outcome>
Rule: Foraging
Scenario: forager returns with nectar
Given a forager bee named <name>
When the forager returns with <volume> milliliters of nectar
Then the hive stores <volume> milliliters of nectarbeehave generate hive_activitytests/features/hive_activity/
├── default_test.py # top-level scenarios (honey production outline)
├── hive_defense_test.py # Rule: Hive defense (guard bee)
└── foraging_test.py # Rule: Foraging (forager returns)
# tests/features/hive_activity/default_test.py
from hypothesis import given, example, strategies as st
@example(nectar=100, rate=20, hours=8, honey=80)
@example(nectar=200, rate=25, hours=12, honey=150)
@example(nectar=50, rate=30, hours=6, honey=35)
@given(nectar=st.integers(), rate=st.integers(), hours=st.integers(), honey=st.integers())
def test_honey_production_from_nectar(nectar, rate, hours, honey):
...# tests/features/hive_activity/hive_defense_test.py
from hypothesis import given, strategies as st
@given(scent=st.text(), outcome=st.text())
def test_guard_bee_inspects_visitor(scent, outcome):
...Note what beehave extracted automatically:
<nectar>,<rate>… →@given()parameters. Strategies inferred from Examples table types (all integers →st.integers()).100,20… →@example()rows from the Examples table."floral"→ enforced literal from step text.checkverifies it appears in the function body.2(from Rule Background2 guards) → enforced literal, inherited by all scenarios in that Rule.<scent>,<outcome>→@given()parameters. No Examples table, so strategy falls back tost.text().
You implement the guard test:
@given(scent=st.text(), outcome=st.text())
def test_guard_bee_inspects_visitor(scent, outcome):
assert "floral" in known_scents()
assert 2 == guard_count()
assert scent in ("floral", "citrus")
assert outcome in ("admitted", "rejected")beehave check hive_activity # check one feature
beehave check # check all featuresRemove the "floral" assertion and check catches it:
tests/features/hive_activity/hive_defense_test.py:4: missing-literal: literal '"floral"' not found in function body
Remove <scent> from the body but keep it as a @given() parameter? Still caught — beehave checks the body only:
tests/features/hive_activity/hive_defense_test.py:4: missing-placeholder: 'scent' not found in function body
Rename the scenario? Both sides are reported:
docs/features/hive_activity.feature:22: unmapped-scenario: scenario 'guard checks visitor' has no test function
tests/features/hive_activity/hive_defense_test.py:4: unmapped-test: 'test_guard_bee_inspects_visitor' has no matching scenario
beehave clean hive_activity # remove unmapped stubs only (safe)
beehave clean hive_activity --force # remove any unmapped functionbeehave list # paths and titles
beehave list -v # include scenario counts, rules, stub status| Check | Severity | What it catches |
|---|---|---|
unmapped-scenario |
error | Scenario has no matching test function |
unmapped-test |
error | Test function has no matching scenario |
missing-placeholder |
error | <placeholder> not referenced in function body |
missing-literal |
error | "string" or numeric literal not in function body |
example-mismatch |
error | Examples row has no matching @example() or vice versa |
misplaced-test |
warning | Function in wrong file (e.g., after Rule removal) |
Warnings exit 0. Errors exit 1. Stubs (bodies with only pass or ...) skip body enforcement until you implement them.
- Scenario title → function name:
Honey Production From Nectar→test_honey_production_from_nectar. Lowercased. Globally unique across all features. - Rule → test file: Top-level scenarios go to
default_test.py. Scenarios inside a Rule go to<rule>_test.py. - Feature title → directory:
Hive Activity→tests/features/hive_activity/. - Strategy inference: Examples table column values are typed — all integers →
st.integers(), all floats →st.floats(), all booleans →st.booleans(), else →st.text(). - Background merging: Feature Background applies to all scenarios. Rule Background applies to that Rule's scenarios only. Background steps cannot contain
<placeholders>. - Literal extraction:
"quoted strings"and numeric tokens in step text are enforced asConstantAST nodes in the function body.
# pyproject.toml
[tool.beehave]
features_dir = "docs/features"
tests_dir = "tests/features"
default_strategy = "text"
background_check_numeric = true
background_check_string = true| Option | Default | Description |
|---|---|---|
features_dir |
docs/features |
Where .feature files live |
tests_dir |
tests/features |
Where generated tests go |
default_strategy |
text |
Fallback strategy for unknown placeholders |
background_check_numeric |
true |
Enforce numeric literals from Background steps |
background_check_string |
true |
Enforce string literals from Background steps |
MIT