Skip to content

Latest commit

 

History

History
254 lines (187 loc) · 10.1 KB

File metadata and controls

254 lines (187 loc) · 10.1 KB
beehave

Python License PyPI

Keep your living documentation and test code in sync — without step definitions.


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 and framework lock-in. beehave eliminates all of that:

  • No step definitions. The function name is the link. Scenario: guard bee inspects visitortest_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
"quoted strings" or 'single-quoted strings' and bare numbers in step text are enforced literals check verifies they appear as Constant nodes in the function body (case-insensitive)
<placeholder> names must be valid Python identifiers, not keywords or builtins They become function parameters (case-insensitive matching)
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. check verifies it appears in the function body.
  • 2 (from Rule Background 2 guards) → enforced literal, inherited by all scenarios in that Rule.
  • <scent>, <outcome>@given() parameters. No Examples table, so strategy falls back to st.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

Show development status

beehave status                    # all features with stage and tree output
beehave status --json             # machine-readable JSON
beehave status --include-unmapped # include unmapped test directories

Output example:

hive_activity (Hive Activity)  needs fixes
├── guard bee inspects visitor                1 error
└── forager returns with nectar              ok

comb_construction (Comb Construction)  ok

dance_language (Waggle Dance Communication)  needs bodies
├── round dance                              no body
└── waggle dance                             no body

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)
invalid-feature-title error Feature title fails charset or word count rules
invalid-rule-title error Rule title fails charset or word count rules
invalid-scenario-title error Scenario title fails charset or word count rules
duplicate-feature-title error Duplicate Feature title (case-insensitive, global)
duplicate-rule-title error Duplicate Rule title or Rule-Feature collision
duplicate-scenario-title error Duplicate Scenario title or Scenario-title collision

beehave check also validates feature, rule, and scenario titles: 2–6 words, [\w\s]+ charset (letters, digits, spaces, underscores), case-insensitive global uniqueness across all title types.


How it maps

  • Scenario title → function name: Honey Production From Nectartest_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 Activitytests/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 'single-quoted strings' and numeric tokens in step text are enforced as Constant AST nodes in the function body. Matching is case-insensitive: "Rex" in Gherkin matches "rex" in test body. Both single and double quote styles are supported. [...] bracket notation inside quotes is preserved verbatim.

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