Spectacular gives your Rust tests three layers of hooks that stack in a predictable order:
| Layer | Runs once per... | Runs per test |
|---|---|---|
| Suite | binary (before) |
test (before_each / after_each) |
| Group | group (before / after) |
test (before_each / after_each) |
| Test | -- | the test body |
Installation
[]
= "0.1"
Quick Start
RSpec-style DSL
use spec;
spec!
describe "string" slugifies the string into a module name ("arithmetic" → mod arithmetic). You can also use mod name directly if you prefer.
Attribute style
use test_suite;
Hooks
Group hooks
Group hooks run within a single test module. before runs once before the first test; after runs once after the last test. before_each and after_each run around every test.
use spec;
use ;
static READY: AtomicBool = new;
spec!
Suite hooks (3-layer)
Suite hooks run across all opted-in groups. Place suite! as a sibling of your test groups, then opt in with suite; (DSL) or #[test_suite(suite)] (attribute style):
use ;
use ;
static DB_READY: AtomicBool = new;
suite!
spec!
Groups without suite; skip the suite layer entirely -- no runtime cost, no coupling.
Context Injection
Hooks can produce context values that flow naturally to tests and teardown hooks, eliminating thread_local! + RefCell patterns.
before→ shared&T: Whenbeforereturns a value, it's stored in anOnceLock<T>. Tests,before_each,after_each, andafterall receive&T.before_each→ ownedT: Whenbefore_eachreturns a value, each test gets an ownedT. The test borrows it throughcatch_unwind, andafter_eachconsumes it for cleanup.
How params are distinguished: Reference params (&T) come from before context. Owned params come from before_each context.
RSpec-style DSL
use spec;
spec!
Attribute style
use ;
Inferred context (no return type)
Hooks can omit their return type and let the macro infer everything from downstream consumers.
before — inferred from &T params:
When before has no -> Type but a downstream hook or test uses an explicit &T param, the macro infers OnceLock<T> automatically:
use spec;
spec!
before_each — inferred from _ params:
When before_each has no return type, the last expression of the body is the context. Tests use _ as the param type and the compiler infers the rest:
use spec;
spec!
Without _ params or &T consumers, hooks with no return type are fire-and-forget as usual.
Hook Execution Order
For each test in a group that opts into suite hooks:
suite::before (Once -- first test in binary triggers it)
group::before (Once -- first test in group triggers it)
suite::before_each
group::before_each
TEST
group::after_each
suite::after_each
group::after (countdown -- last test in group triggers it)
After-hooks are protected by catch_unwind, so cleanup runs even if a test panics.
Async Tests
Both spec! and #[test_suite] support async test cases and hooks. Specify a runtime (tokio or async_std) to enable async:
use spec;
spec!
Feature-based default: Enable the tokio or async-std feature on spectacular to auto-detect the runtime:
[]
= { = "0.1", = ["tokio"] }
With the feature enabled, async it / async fn test cases Just Work without explicit tokio; or #[test_suite(tokio)].
Attribute Style Reference
| Attribute | Description |
|---|---|
#[test_suite] |
Marks a module as a test group |
#[test_suite(suite)] |
Same, with suite hook opt-in |
#[test_suite(tokio)] |
Async test group with tokio runtime |
#[test] |
Marks a function as a test |
#[before] |
Once-per-group setup (max one per module) |
#[after] |
Once-per-group teardown (max one per module) |
#[before_each] |
Per-test setup (max one per module) |
#[after_each] |
Per-test teardown (max one per module) |
Context Injection Reference
spec! syntax
| Form | Description |
|---|---|
describe "name" { } |
BDD-style group (string slugified to module name) |
mod name { } |
Group with explicit module name |
before -> Type { } |
Run-once setup returning shared context (explicit) |
before { } |
Run-once setup with inferred context (when consumers use &T params) |
after |name: &Type| { } |
Run-once teardown receiving shared context |
before_each |name: &Type| -> Type { } |
Per-test setup with shared context input, owned output |
before_each { } |
Per-test setup with inferred context (when tests use _ params) |
after_each |name: &Type, name: Type| { } |
Per-test teardown with shared + owned context |
it "desc" |name: &Type, name: Type| { } |
Test with shared + owned context |
Attribute syntax
| Pattern | Description |
|---|---|
fn init() -> T |
#[before] returning shared context (explicit) |
fn init() |
#[before] with inferred context (when consumers use &T params) |
fn cleanup(x: &T) |
#[after] receiving shared context |
fn setup(x: &T) -> U |
#[before_each] with shared input, owned output |
fn setup() |
#[before_each] with inferred context (when tests use _ params) |
fn teardown(x: &T, y: U) |
#[after_each] with shared + owned |
fn test_name(x: &T, y: U) |
#[test] with shared + owned |
License
MIT -- see LICENSE for details.