behave-kit
August 8, 2026 · View on GitHub
The Swiss-army knife for Behave — soft assertions, typed context, conditional skip, environment management, fixtures and more.
Why behave-kit?
Behave is great for BDD, but real test suites need more than Given/When/Then. You need to:
- Collect multiple assertion failures without stopping at the first one
- Skip steps based on environment, OS, or missing dependencies
- Read environment variables with type conversion and validation
- Load test data from CSV, JSON, YAML, or Excel files
- Manage fixtures with automatic setup/teardown by tag
- Get IDE autocompletion for context attributes
- Debug failures with automatic context dumps
- Poll for conditions with configurable timeout and interval
- Assert expected exceptions as part of soft assertion collections
- Run data-driven steps from CSV, JSON, YAML, or Excel files
- Isolate environment variables with snapshot/restore context managers
- Navigate nested dicts with dot notation
- Assert execution time with
assert_underand@timed - Create temp directories for filesystem-isolated tests
- Continue after failed steps for comprehensive test reporting
- Execute sub-steps with outline substitution and state isolation
- Class-based steps — define Given/When/Then as methods on a class with
self.context, lifecycle hooks, and per-step matchers - Per-scenario timeout —
@timeout:Ntags override a default, platform-aware (SIGALRM on Unix, threading.Timer on Windows)
behave-kit provides all of these as independent, opt-in utilities — no monkey-patching, no breaking changes.
Installation
pip install behave-kit
With optional extras:
pip install "behave-kit[yaml,excel,dotenv]"
| Extra | Provides | Dependency |
|---|---|---|
yaml | YAML data loading | pyyaml>=6.0 |
excel | XLSX data loading | openpyxl>=3.1 |
dotenv | .env file loading | python-dotenv>=1.0 |
docs | Sphinx documentation build | sphinx, furo, myst-parser |
dev | Development tools | pytest, ruff, mypy, build, pre-commit |
Quickstart
Level 1 — Automatic wiring
Add two lines to your environment.py and every feature is wired automatically:
from behave_kit import setup, teardown
def before_all(context):
setup(context, env="staging")
def after_scenario(context, scenario):
teardown(context)
Level 2 — Cherry-pick
Import only what you need:
from behave_kit import assert_soft, env, load_data
@then("the response should be valid")
def step(context):
assert_soft(context.response.status_code == 200)
api_key = env("API_KEY", required=True)
users = load_data("tests/data/users.csv")
Level 3 — Namespace
import behave_kit as bk
@then("the response should be valid")
def step(context):
bk.assert_soft(context.response.status_code == 200)
Features
Soft assertions
Collect multiple failures and report them all at once:
from behave_kit import assert_soft
assert_soft(response.status_code == 200)
assert_soft(response.body["count"] > 0)
assert_soft("error" not in response.body)
# All failures reported together at teardown
TypedContext
Schema-validated proxy with IDE autocompletion and mypy support:
from behave_kit import TypedContext
class MySchema:
driver: str
base_url: str
ctx = TypedContext(context, MySchema)
ctx.setup(driver="chrome", base_url="https://test.com")
print(ctx.driver) # IDE autocompletion works
Conditional skip
Skip steps by environment, OS, or missing dependency:
from behave_kit import skip_if_env, skip_if_no_browser, skip_on_os, skip_if_missing
@skip_if_env("production")
@when("I run the staging-only step")
def step(context): ...
@skip_if_no_browser
@when("I open the browser")
def step(context): ...
@skip_on_os("windows")
@when("I run the unix-only step")
def step(context): ...
@skip_if_missing("selenium")
@when("I use selenium")
def step(context): ...
Environment variables
Typed reads with defaults, validation, and config file fallback:
from behave_kit import env
api_key = env("API_KEY", required=True) # str
timeout = env("TIMEOUT", var_type=int, default=30) # int
debug = env("DEBUG", var_type=bool, default=False) # bool
Data loading
Single API for CSV, JSON, YAML, and Excel:
from behave_kit import load_data
users = load_data("tests/data/users.csv") # list[dict]
config = load_data("tests/data/config.json") # dict
sheet = load_data("tests/data/sheet.xlsx") # list[dict] (requires [excel])
Fixtures
Tag-based setup/teardown with dependency resolution:
from behave_kit import fixture
@fixture("browser")
def browser_fixture(context):
def setup(ctx):
ctx.browser = start_browser()
def teardown(ctx):
ctx.browser.quit()
return (setup, teardown)
@fixture("database", requires="browser")
def database_fixture(context): ...
Context dump
Automatic context snapshot on scenario failure for easier debugging:
from behave_kit import dump_context_on_failure
# Wired automatically by setup(), or call manually
Step suggestions
"Did you mean?" hints for undefined steps:
from behave_kit import setup_suggestions
# Wired automatically by setup()
Scoped attributes
Automatic cleanup of context attributes per scenario:
from behave_kit import scoped
@scoped("driver")
@when("I start the driver")
def step(context):
context.driver = start_driver()
# "driver" is automatically deleted after the scenario
Conditional steps
Run a step only when a condition holds:
from behave_kit import when_if
@when_if(lambda ctx: ctx.config.env == "staging")
@when("I run the staging-only step")
def step(context): ...
Parameter types
Register custom Behave parameter types:
from behave_kit import parameter_type
@parameter_type("User", r"[\w.]+@[\w.]+\.[a-z]+")
def parse_user(text):
return User(name=text)
Class-based steps
Define step implementations as methods on a class, with per-scenario instances, lifecycle hooks, and per-step matcher selection:
from behave_kit import step_impl_base
Base = step_impl_base()
class AccountSteps(Base):
@Base.given("I have a balance of {amount:d}")
def set_balance(self, amount):
self.balance = amount
@Base.when("I deposit {amount:d}")
def deposit(self, amount):
self.balance += amount
@Base.then("the balance should be {expected:d}")
def check_balance(self, expected):
assert self.balance == expected
@property
def balance(self):
return getattr(self.context, "balance", 0)
@balance.setter
def balance(self, value):
self.context.balance = value
def setup(self):
# Called once when the instance is created for a scenario
pass
def teardown(self):
# Called after the scenario ends (via teardown_steps / teardown)
pass
AccountSteps.register()
self.context is bound automatically — no context parameter needed.
Subclass to extend or override steps; register only the most-derived class.
Pass matcher=RegexMatcher to a decorator for per-step matcher selection.
Soft exception assertions
Check that a callable raises an expected exception, collected as a soft failure:
from behave_kit import assert_soft_raises
assert_soft_raises(ValueError, lambda: int("abc"))
assert_soft_raises(KeyError, lambda: {}["missing"])
# Failures are collected and reported together at teardown
Data-driven steps
Run a step once per row of a data file, with column names injected as keyword arguments:
from behave_kit import data_driven
@data_driven("tests/data/users.csv")
@when("I login as {username}")
def step(context, username=None, password=None):
login(username, password)
# Runs once per row in users.csv
Environment variable snapshot
Save and restore os.environ so tests don't leak environment variable changes:
from behave_kit import env_snapshot
with env_snapshot():
os.environ["API_KEY"] = "test"
# ... test ...
# API_KEY restored automatically
Dict navigation with get_path
Extract values from nested dicts using dot notation:
from behave_kit import get_path
city = get_path(response, "user.address.city") # "Berlin"
name = get_path(response, "users.0.name", default="?") # "Alice"
Time assertions
Assert that a callable completes within a time limit:
from behave_kit import assert_under, timed
assert_under(2.0, lambda: client.get("/health"))
@timed(1.5)
@when("I fetch the data")
def step(context): ...
Wait until
Poll a condition until it becomes true or a timeout is reached:
from behave_kit import wait_until
wait_until(lambda: context.response.status_code == 200, timeout=10, interval=0.5)
Temp workspace
Create an isolated temporary directory and restore the CWD on exit:
from behave_kit import temp_workspace
with temp_workspace() as tmp:
config_path = tmp / "config.json"
config_path.write_text("{}")
# Directory is cleaned up automatically
Continue after failed step
Control whether scenarios keep running remaining steps after a failure:
from behave_kit import continue_after_failed, continue_on_failure
# Enable globally
continue_after_failed(True)
# Or temporarily via context manager
with continue_on_failure():
# scenarios inside this block continue after failed steps
...
Or wire it through setup():
from behave_kit import setup
def before_all(context):
setup(context, continue_after_failed=True)
Sub-step execution with isolation
Execute Gherkin sub-steps with Scenario Outline variable substitution and guaranteed table/text restoration:
from behave_kit import run_steps
@when("I complete the checkout flow")
def step_impl(context):
run_steps(
context,
"""
Given I have items in my cart
When I enter shipping info for "<city>"
Then I should see the order confirmation
""",
)
# context.table and context.text are preserved after execution
Per-scenario timeout
Set a default timeout and override it per scenario or feature with @timeout:N tags:
from behave_kit import setup_timeout
from behave_kit.timeout import timeout_before_scenario, timeout_after_scenario
def before_all(context):
setup_timeout(context, default_timeout=30)
def before_scenario(context, scenario):
timeout_before_scenario(context, scenario)
def after_scenario(context, scenario):
timeout_after_scenario(context, scenario)
@timeout:10
Scenario: Fast scenario with tag override
When I do something quick
@timeout:0
Scenario: Disable timeout for this scenario
When I do something slow
@timeout:60
Feature: Feature-level timeout inherits to all scenarios
- Unix: uses
signal.SIGALRMfor immediate interruption. - Windows: uses
threading.Timerfallback (detected after step finishes). @timeout:0disables the timeout for that scenario.- Feature-level tags inherit to all scenarios; scenario tags override feature tags.
You can also set the default via the BEHAVE_SCENARIO_TIMEOUT environment
variable. If you call setup_timeout(context) without default_timeout, the
value is read from the environment (default 0):
BEHAVE_SCENARIO_TIMEOUT=30 behave
Documentation
Full documentation is available at mathiaspaulenko.github.io/behave-kit.
To build docs locally:
pip install "behave-kit[docs]"
cd docs && sphinx-build -b html . _build/html
Development
See CONTRIBUTING.md for development setup, coding standards, and contribution guidelines.
License
MIT — see LICENSE.