Skip to main content

Crate spectacular

Crate spectacular 

Source
Expand description

An RSpec-inspired test framework for Rust with stackable before/after hooks.

Spectacular provides three layers of test hooks that stack in a predictable order:

LayerRuns once per…Runs per test
Suitebinary (before)test (before_each / after_each)
Groupgroup (before / after)test (before_each / after_each)
Testthe test body

§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)

Groups without suite; skip the suite layer entirely.

§Quick Start

use spectacular::spec;

spec! {
    mod arithmetic {
        it "adds two numbers" {
            assert_eq!(2 + 2, 4);
        }

        it "multiplies two numbers" {
            assert_eq!(3 * 7, 21);
        }
    }
}

§Group Hooks

use spectacular::spec;
use std::sync::atomic::{AtomicBool, Ordering};

static READY: AtomicBool = AtomicBool::new(false);

spec! {
    mod with_hooks {
        use super::*;

        before { READY.store(true, Ordering::SeqCst); }

        it "runs after setup" {
            assert!(READY.load(Ordering::SeqCst));
        }
    }
}

§Suite Hooks (3-Layer)

Place suite! as a sibling of your test groups, then opt in with suite; (in spec!) or #[test_suite(suite)] (attribute style):

use spectacular::{suite, spec};
use std::sync::atomic::{AtomicBool, Ordering};

static DB_READY: AtomicBool = AtomicBool::new(false);

suite! {
    before { DB_READY.store(true, Ordering::SeqCst); }
}

spec! {
    mod database_tests {
        use super::*;
        suite;

        it "has database access" {
            assert!(DB_READY.load(Ordering::SeqCst));
        }
    }
}

§Attribute Style

For those who prefer standard Rust attribute syntax — just use #[test]:

use spectacular::{test_suite, before};

#[test_suite]
mod my_tests {
    #[before]
    fn setup() { }

    #[test]
    fn it_works() {
        assert_eq!(1 + 1, 2);
    }
}

§Async Tests

Both spec! and #[test_suite] support async test cases and hooks. Specify a runtime (tokio or async_std) to enable async:

spec! style:

use spectacular::spec;

spec! {
    mod my_async_tests {
        tokio;  // or async_std;

        async before_each { db_connect().await; }

        async it "fetches data" {
            let result = fetch().await;
            assert!(result.is_ok());
        }

        it "sync test works too" {
            assert_eq!(1 + 1, 2);
        }
    }
}

Attribute style:

use spectacular::{test_suite, before_each};

#[test_suite(tokio)]
mod my_async_tests {
    #[before_each]
    async fn setup() { db_connect().await; }

    #[test]
    async fn it_works() {
        let result = fetch().await;
        assert!(result.is_ok());
    }
}

Async after_each hooks are panic-safe — they run even if the test body panics, using an async-compatible catch_unwind wrapper.

Feature-based default: If you enable the tokio or async-std feature on spectacular, async tests auto-detect the runtime so you can omit the explicit tokio; / #[test_suite(tokio)] argument:

[dev-dependencies]
spectacular = { version = "0.1", features = ["tokio"] }

With the feature enabled, async it / async fn test cases Just Work. Explicit runtime arguments always take precedence over the feature default. If both features are enabled simultaneously, you must specify explicitly (the macro will emit a compile error).

§Context Injection

Hooks can produce context values that flow naturally to tests and teardown hooks, eliminating the need for thread_local! + RefCell patterns.

§before → shared &T via OnceLock

When before returns a value, it’s stored in a OnceLock<T>. Tests, before_each, after_each, and after all receive &T.

§before_each → owned T per test

When before_each returns a value, each test gets an owned T. The test borrows it through catch_unwind, and after_each consumes it for cleanup.

How params are distinguished: Reference params (&T) come from before context. Owned params come from before_each context.

spec! style:

use spectacular::spec;

spec! {
    mod my_tests {
        tokio;

        before -> PgPool {
            PgPool::connect("postgres://...").unwrap()
        }

        after |pool: &PgPool| {
            pool.close();
        }

        async before_each |pool: &PgPool| -> TestContext {
            TestContext::seed(pool).await
        }

        async after_each |pool: &PgPool, ctx: TestContext| {
            ctx.cleanup(pool).await;
        }

        async it "creates a team" |pool: &PgPool, ctx: TestContext| {
            // pool from before (shared &ref), ctx from before_each (owned)
        }
    }
}

Attribute style:

use spectacular::{test_suite, before, after, before_each, after_each};

#[test_suite(tokio)]
mod my_tests {
    #[before]
    fn init() -> PgPool {
        PgPool::connect("postgres://...").unwrap()
    }

    #[after]
    fn cleanup(pool: &PgPool) {
        pool.close();
    }

    #[before_each]
    async fn setup(pool: &PgPool) -> TestContext {
        TestContext::seed(pool).await
    }

    #[after_each]
    async fn teardown(pool: &PgPool, ctx: TestContext) {
        ctx.cleanup(pool).await;
    }

    #[test]
    async fn test_create_team(pool: &PgPool, ctx: TestContext) {
        // pool from before (shared &ref), ctx from before_each (owned)
    }
}

§Inferred context (no return type)

When before_each has no return type, the last expression of the body is the context. Tests and after_each use _ as the param type to receive it. The macro detects _-typed params and inlines the body automatically.

Without _ params, a void before_each is fire-and-forget as usual.

Modules§

prelude
Convenience re-export of all spectacular macros.

Macros§

spec
Defines a test group using RSpec-style DSL.
suite
Defines suite-level hooks that run across all opted-in test groups.

Attribute Macros§

after
Marks a function as a once-per-group teardown hook inside a #[test_suite] module.
after_each
Marks a function as a per-test teardown hook inside a #[test_suite] module.
before
Marks a function as a once-per-group setup hook inside a #[test_suite] module.
before_each
Marks a function as a per-test setup hook inside a #[test_suite] module.
test_suite
Marks a module as a test suite using standard Rust attribute syntax.