A thin layer on Hypothesis for Gherkin-style BDD testing with vocabulary enforcement
Project description
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 beehave
How it differs from standard Gherkin tools
Traditional 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 |
Usage
Write a feature
# 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 nectar
Generate stubs
beehave generate hive_activity
tests/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().
Check consistency
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 features
Remove 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
Clean up stale functions
beehave clean hive_activity # remove unmapped stubs only (safe)
beehave clean hive_activity --force # remove any unmapped function
List features
beehave list # paths and titles
beehave list -v # include scenario counts, rules, stub status
What check enforces
| 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.
How it maps
- Scenario title → function name:
Honey Production From Nectar→test_honey_production_from_nectar. 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.
Configuration
# 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 |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file beehave-0.3.1.tar.gz.
File metadata
- Download URL: beehave-0.3.1.tar.gz
- Upload date:
- Size: 54.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b72e1ac26aa2944fa37f717e510acd538ea52dff455d1c91dcbe2749c1728400
|
|
| MD5 |
80762c055ed87f59efa5c48826302897
|
|
| BLAKE2b-256 |
333edc4680fb9a72aa66bf1bdfbdc505ddfbe9fbcb7c97911d9d8f8452c7944c
|
Provenance
The following attestation bundles were made for beehave-0.3.1.tar.gz:
Publisher:
pypi-publish.yml on nullhack/beehave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
beehave-0.3.1.tar.gz -
Subject digest:
b72e1ac26aa2944fa37f717e510acd538ea52dff455d1c91dcbe2749c1728400 - Sigstore transparency entry: 1524564872
- Sigstore integration time:
-
Permalink:
nullhack/beehave@6c83cd6ba9f09a8cf7729488883f11a688b76cdc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nullhack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@6c83cd6ba9f09a8cf7729488883f11a688b76cdc -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file beehave-0.3.1-py3-none-any.whl.
File metadata
- Download URL: beehave-0.3.1-py3-none-any.whl
- Upload date:
- Size: 17.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5843442186a8aed1d3bb91effbe7595682d99191e323698f1a05a58d14c12616
|
|
| MD5 |
645aa353e34dbb5ae3d238a0721d7464
|
|
| BLAKE2b-256 |
0457de54a49f9135a75026408930a28d921760a4e41835a23309a0ad1248cc5c
|
Provenance
The following attestation bundles were made for beehave-0.3.1-py3-none-any.whl:
Publisher:
pypi-publish.yml on nullhack/beehave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
beehave-0.3.1-py3-none-any.whl -
Subject digest:
5843442186a8aed1d3bb91effbe7595682d99191e323698f1a05a58d14c12616 - Sigstore transparency entry: 1524564885
- Sigstore integration time:
-
Permalink:
nullhack/beehave@6c83cd6ba9f09a8cf7729488883f11a688b76cdc -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nullhack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@6c83cd6ba9f09a8cf7729488883f11a688b76cdc -
Trigger Event:
workflow_run
-
Statement type: