behave is a behavior-driven testing library for Rust. It gives you a behave!
macro for nested, readable test suites and an expect! API for expressive
assertions, while still compiling down to ordinary #[test] functions.
Use behave when you want test code that reads like scenarios instead of a flat
list of unrelated unit tests:
- nested groups instead of large test modules
setupblocks that flow into child scenarioseachblocks for parameterized/table-driven testsmatrixblocks for Cartesian product test generationxfailfor expected-failure teststagmetadata for grouping and filtering testsskip_when!for runtime conditional skipping- built-in matchers for equality, strings, collections, options, results, and floats
pendingandfocusmarkers for test workflow- optional
cargo-behaveCLI with tree/JSON/JUnit output, watch mode, tag filtering, and flaky-test detection
behave! is a proc macro. At compile time it turns your scenario tree into
standard Rust test functions, so cargo test still runs the suite and there is
no custom runtime to keep alive.
Add the crate as a dev-dependency:
cargo add behave --devCreate tests/behave_smoke.rs:
use behave::prelude::*;
behave! {
"checkout totals" {
setup {
let prices = [120, 80, 40];
let subtotal: i32 = prices.iter().sum();
}
"adds line items" {
expect!(subtotal).to_equal(240)?;
}
"renders a receipt line" {
let receipt = format!("subtotal={subtotal}");
expect!(receipt).to_contain_substr("240")?;
}
}
pending "applies coupon codes" {}
}Run it:
cargo testThat is the whole onboarding path. The generated tests are normal #[test]
items, so you can keep using the usual Rust tooling around them.
Use each to generate one test per case. Each case becomes its own #[test]
function, so failures tell you exactly which input broke:
use behave::prelude::*;
behave! {
"addition" {
each [
(2, 2, 4),
(0, 0, 0),
(-1, 1, 0),
] |a, b, expected| {
expect!(a + b).to_equal(expected)?;
}
}
}This generates addition::case_0, addition::case_1, and addition::case_2.
Single-parameter cases work too:
use behave::prelude::*;
behave! {
"Fibonacci numbers are positive" {
each [1, 1, 2, 3, 5, 8, 13] |n| {
expect!(n).to_be_greater_than(0)?;
}
}
}each inherits setup, teardown, and tokio; from the parent group:
use behave::prelude::*;
behave! {
"tax calculation" {
setup {
let tax_rate = 0.08_f64;
}
"computes total with tax" {
each [
(100.0_f64, 108.0_f64),
(50.0_f64, 54.0_f64),
(0.0_f64, 0.0_f64),
] |price, expected| {
let total = price.mul_add(tax_rate, price);
expect!(total).to_approximately_equal(expected)?;
}
}
}
}See examples/parameterized.rs for the full
working example.
setup bindings flow from parent groups into child scenarios and child
setup blocks. This avoids duplicating shared state:
use behave::prelude::*;
behave! {
"order pricing" {
setup {
let items = vec![1200, 800, 350];
let total: i64 = items.iter().sum();
}
"subtotal sums line items" {
expect!(total).to_equal(2350)?;
}
"with 10% discount" {
setup {
let discounted = total - (total * 10 / 100);
}
"applies percentage" {
expect!(discounted).to_equal(2115)?;
}
"with shipping" {
setup {
let final_total = discounted + 500;
}
"adds flat fee" {
expect!(final_total).to_equal(2615)?;
}
}
}
}
}See examples/setup_inheritance.rs for
a fuller version with helper functions and shadowing.
teardown blocks run after every test in the group, even if the test panics (sync tests) or returns an error (async tests). Use them for cleanup:
use behave::prelude::*;
behave! {
"database tests" {
setup {
let pool = vec!["conn_1"];
}
teardown {
// Runs after the test body (panic-safe in sync mode).
drop(pool);
}
"connection is available" {
expect!(pool).to_have_length(1)?;
}
}
}Inner teardowns run before outer teardowns (like destructors). See
examples/teardown.rs for nested teardown patterns.
Create a new project and try behave in one go:
cargo new behave-demo
cd behave-demo
cargo add behave --dev
mkdir -p testsThen put the Quick Start example above into tests/behave_smoke.rs and run
cargo test.
Install the optional CLI:
cargo install behave --features cliRun the suite with tree output:
cargo behaveRun only tests tagged slow:
cargo behave --tag slowWatch for file changes and re-run:
cargo behave --watchEmit a machine-readable report:
cargo behave --output json
cargo behave --output junit| Feature | Default | Description |
|---|---|---|
std |
Yes | Standard library support |
cli |
No | Enables cargo-behave and flaky-test utilities |
color |
No | ANSI-colored diff output for assertion failures |
regex |
No | to_match_regex and to_contain_regex matchers |
tokio |
No | Enables tokio; async test generation |
| Macro | Description | Docs |
|---|---|---|
behave! |
BDD test suite DSL with groups, setup, teardown, each, matrix, tags, and more | Reference |
expect! |
Wrap a value for matcher assertions with structured error messages | Reference |
expect_panic! |
Assert that an expression panics | Reference |
expect_no_panic! |
Assert that an expression does not panic | Reference |
skip_when! |
Conditionally skip a test at runtime with a reason | Reference |
| Category | Matchers |
|---|---|
| Equality | to_equal, to_not_equal |
| Boolean | to_be_true, to_be_false |
| Ordering | to_be_greater_than, to_be_less_than, to_be_at_least, to_be_at_most |
| Option | to_be_some, to_be_none, to_be_some_with |
| Result | to_be_ok, to_be_err, to_be_ok_with, to_be_err_with |
| Collections | to_contain, to_be_empty, to_not_be_empty, to_have_length, to_contain_all_of |
| Strings | to_start_with, to_end_with, to_contain_substr, to_have_str_length |
| Float | to_approximately_equal, to_approximately_equal_within |
| Panic | expect_panic!, expect_no_panic! |
| Predicate | to_satisfy |
| Custom | to_match with BehaveMatch |
| Regex (feature) | to_match_regex, to_contain_regex |
Map (HashMap, BTreeMap) |
to_contain_key, to_contain_value, to_contain_entry, to_be_empty, to_not_be_empty, to_have_length |
| Composition | all_of, any_of, not_matching |
All matchers respect .not() / .negate().
The matcher docs live in docs/matchers/ with one page per matcher (plus a quick index).
| Example | What it shows |
|---|---|
examples/quickstart.rs |
Recommended first suite with setup, matchers, and pending |
examples/parameterized.rs |
each blocks with multi-param tuples, single params, and inherited setup |
examples/setup_inheritance.rs |
Three levels of nested setup with a realistic pricing domain |
examples/teardown.rs |
Panic-safe cleanup, nested teardowns, and resource management |
examples/custom_matcher.rs |
Reusable BehaveMatch<T> matcher type with negation |
tests/smoke.rs |
Full DSL and matcher surface coverage |
You can:
- nest groups freely
- share bindings from a parent
setupinto child scenarios - shadow a setup binding with a later
letin a childsetupor scenario body - use
eachblocks for parameterized/table-driven test generation - use
teardownblocks for cleanup after each test (panic-safe in sync, error-safe in async) - declare
tokio;in a group to generate#[tokio::test]async tests (requirestokiofeature) - use
cargo testnormally because generated tests are ordinary#[test]functions - use
cargo behavefor tree output, filters, and libtest flags - use
cargo behave --output jsonorcargo behave --output junitfor CI-friendly reports - use
cargo behave --manifest-path path/to/Cargo.tomlor--package namein workspaces - use
cargo behave --tag slowto run only tagged tests,--exclude-tag flakyto exclude - use
cargo behave --focusto run only focused tests - use
cargo behave --fail-on-focusto reject focused tests in CI - use
cargo behave --watchto re-run on file changes - use
skip_when!(condition, "reason")to conditionally skip tests at runtime
Current limitations:
- one
setupblock per group, oneteardownblock per group - DSL order within a group:
tokio;→timeout→setup {}→teardown {}→ children pendingblocks must be empty- async teardown is error-safe but not panic-safe (no
catch_unwindacross.awaitpoints)
The current trust signals are intentionally concrete:
behave!compiles to ordinary#[test]functions- runnable examples live in
examples/and are exercised in tests - public docs, doctests, Clippy, and rustdoc warnings are checked together
unsafeis forbidden by lint configuration- limitations are documented explicitly instead of left implicit
- security reporting is documented in SECURITY.md
For the fuller trust and maintenance picture, see docs/RELIABILITY.md.
Create behave.toml in your project root:
[flaky_detection]
enabled = true
history_file = ".behave/history.json"
consecutive_passes = 5When enabled, cargo behave records past outcomes and warns when a test fails
after many consecutive passes without source changes in the selected package set.
Add .behave/ to .gitignore.
- User Guide
- Macro Reference —
behave!|expect!|expect_panic!|expect_no_panic!|skip_when! - Matcher Reference
- CLI Guide
- Reliability
- Architecture
- Contributing
- API docs on docs.rs
See SECURITY.md for the reporting process.
Licensed under the Apache License, Version 2.0. See LICENSE.