test-better
Result-returning tests with
?: composable matchers, rich failure output, and never a.unwrap()in sight.
test-better is a testing library for Rust that treats ? as the control-flow
operator of tests. Instead of panicking with .unwrap(), .expect("..."), and
assert_eq!, you write tests that return Result and use composable,
intention-revealing matchers. When something fails you get the expression that
failed, the expected and actual values, the source location, and the full
context chain, all rendered as a value rather than a panic.
It works with the stock cargo test harness (no runtime required), stays
runtime-agnostic for async code, and grows from primitive assertions up through
async, property, snapshot, and parameterized testing without changing how a
test is shaped.
Quick start
Add the facade crate to your dev-dependencies:
[]
= "0.1"
Write a test that returns TestResult and reach for ?:
use *;
When expect! fails, the message names the expression, both sides of the
comparison, and where it happened, with no backtrace through the harness:
expectation failed: expect!(port).to(eq(8080))
expected: 8080
actual: 8000
at tests/config.rs:12
Why ? instead of panic!
A panicking assertion throws away everything except a message. A
Result-returning test keeps the context:
- Failures are values. A
Matcher<T>is a value you can pass around, negate, and combine, not a statement that aborts the thread. - Context is never erased. Attach a human-readable note with
.context("...")or.or_fail_with("...")and it travels with the error. ?composes. Setup that can fail, the assertion itself, and teardown all use the same operator. No nestedmatch, no.unwrap()to "just get past" the setup.- No required runtime. Tests run under plain
cargo test. A prettier grouped-output runner is available but never mandatory.
What's in the box
use *;
// Composable matchers over any type
expect!.to?;
expect!.to?;
expect!.to?;
// Structural matching on structs and enums
expect!.to?;
expect!.to?;
// Soft assertions: collect several failures, report them together
soft?;
Async, property, and snapshot testing are layered on the same expect!/?
shape:
// Async timing assertions, runtime-agnostic
expect!.to_complete_within.await?;
// Property testing over generated inputs
property!?;
// Inline and file snapshots, with redactions
expect!.to_match_inline_snapshot?;
Documentation
- The
test-betterbook is the prose guide: Getting Started, Migrating fromassert!, Writing Matchers, Async Testing, Property Testing, Snapshots, Fixtures, Performance, and Recipes. The sources live underbook/; build a local copy withmdbook build book. - The API reference is the rustdoc:
cargo doc --open -p test-better. - Runnable examples live in
examples/, each a small crate with its own test suite:cargo test -p web-handler-tests-exampleand friends.
Workspace layout
test-better is the facade you depend on; it re-exports everything through its
prelude. The functionality is split across focused crates so you only compile
what you use.
| Crate | Purpose |
|---|---|
test-better |
Facade crate: re-exports and prelude. |
test-better-core |
TestError, TestResult, ContextExt, OrFail. |
test-better-matchers |
Matcher trait, standard matchers, expect!. |
test-better-macros |
Procedural macros (matches_struct!, #[test_case], fixtures). |
test-better-async |
Async and timing helpers (runtime-gated). |
test-better-property |
Property-testing bridge (proptest-backed). |
test-better-snapshot |
Snapshot testing, inline and file-based. |
test-better-runner |
Optional cargo-test-better pretty runner. |
Contributing
Bug reports, documentation fixes, new matchers, and feature work are all welcome. See CONTRIBUTING.md for the design principles, local check commands, and the definition of done.
License
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.