Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chicago TDD Tools Pattern Cookbook

Welcome to the Chicago TDD Tools Pattern Cookbook. This mdBook captures the living pattern language behind the framework. Inspired by Christopher Alexander's "A Pattern Language," each pattern documents a recurring problem and the high-leverage solution encoded in the framework. Patterns are organized across testing, architecture, and design themes so you can quickly discover what fits your current context.

Every pattern includes:

  • Context – when the pattern applies
  • Problem – the tension you are trying to resolve
  • Solution – the structure that resolves the tension, with concrete code
  • Forces – trade-offs the solution balances
  • Examples – copy-pasteable Rust using Chicago TDD Tools
  • Related Patterns – how the language connects

Use this cookbook as a field guide while building and testing Rust systems with Chicago TDD Tools.

Introduction

Christopher Alexander famously observed that great environments share a pattern language – a network of proven responses to recurring forces. Chicago TDD Tools embodies the same philosophy for Rust testing. Instead of isolated utilities, the framework codifies high-leverage patterns that push teams toward dependable, behavior-focused tests and extendable architecture.

This cookbook distills those patterns. Each entry is written in Alexander's form so you can quickly scan the context, recognize the tension, and apply the solution. Read the patterns sequentially to see how they reinforce each other, or jump to the problem you have today.

The language is organized into three families:

  1. Testing Patterns – maintainable, behavior-driven tests that fail fast and verify real outcomes.
  2. Architecture Patterns – structural choices that keep the framework extensible and consistent.
  3. Design Patterns – type-level techniques, zero-cost abstractions, and compile-time validation.

Combine these ingredients to build resilient Rust systems aligned with Chicago TDD principles: state-based testing, real collaborators, behavior verification, and the AAA pattern.

Testing Patterns

Tests are executable specifications. The Chicago TDD Tools testing patterns capture how to write fast, trustworthy, and expressive tests that verify observable behavior. Each pattern layers on the previous ones; together they reflect the AAA mindset, the insistence on real collaborators, and the bias toward fast failure.

Use these patterns when a test feels brittle, slow, or unclear. The language highlights which macro to reach for, how to structure the test body, and how to keep failures meaningful.

Pattern 1: AAA Pattern

Context

You are writing or reviewing a test in Chicago TDD Tools. You want the test to communicate intent instantly and fail with a precise message when behavior regresses.

Problem

Tests that intermingle setup, behavior, and assertions become hard to scan. When the failure occurs, teammates must untangle implicit state, which slows feedback and hides missing assertions.

Solution

Structure every test body into three explicit phases – Arrange, Act, Assert – and let the framework enforce it. Use the test!, async_test!, or fixture_test! macros so that comments and code read in a top-to-bottom narrative.

Forces

  • Readability vs. flexibility: expressive labels without duplicating boilerplate
  • Fast diagnosis vs. runtime overhead: compile-time enforcement should cost nothing
  • Behavior proof vs. implementation detail: assertions must verify observable outcomes

Examples

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;

test!(test_scaling_multiplier, {
    // Arrange
    let multiplier = 3;
    let input = 7;

    // Act
    let result = multiplier * input;

    // Assert
    assert_eq!(result, 21, "multiplier should scale the input");
});
}

Async tests follow the same skeleton:

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;

async_test!(test_fetch_customer, {
    // Arrange
    let client = FakeCrmClient::connected();

    // Act
    let customer = client.fetch("cust-123").await?;

    // Assert
    assert_eq!(customer.id, "cust-123");
    Ok(())
});
}
  • Pattern 2: Error Path Testing
  • Pattern 5: Real Collaborators
  • Pattern 11: Zero-Cost Abstractions

Pattern 2: Error Path Testing

Context

A function returns Result or Option, and the happy path already behaves as expected. You must guarantee that every failure mode is observable and carries actionable context.

Problem

When tests only exercise the success path, regressions sneak in through unhandled errors, missing context, or broken guardrails. Production discovers the failure first, and the team spends time reconstructing the scenario.

Solution

Enumerate every documented error variant and write a focused test for each. Use test! or param_test! to drive the error case, assert the specific variant, and check that error messages include the necessary context. Prefer direct construction or builders over mocks so that you validate the real failure.

Forces

  • Coverage vs. duplication: each error merits a separate assertion without creating noise
  • Realistic behavior vs. test speed: use in-memory collaborators where possible, but ensure pathways stay intact
  • Diagnostic clarity vs. maintenance: error text should be specific yet stable

Examples

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;
use chicago_tdd_tools::core::fixture::FixtureError;

param_test! {
    #[case("", FixtureError::CreationFailed("name required".into()))]
    #[case("db://unreachable", FixtureError::OperationFailed("reconnect failed".into()))]
    fn test_fixture_error_paths(input: &str, expected: FixtureError) {
        let result = TestFixture::with_data(input.to_string()).cleanup();
        assert_err!(&result);
        let error = result.unwrap_err();
        assert_eq!(format!("{}", error), format!("{}", expected));
    }
}
}
  • Pattern 3: Boundary Conditions
  • Pattern 18: Timeout Defense in Depth
  • Pattern 19: Feature Gate Slices

Pattern 3: Boundary Conditions

Context

Logic depends on limits: minimum quantities, maximum run lengths, exclusive ranges. You must guarantee behavior at and around those boundaries.

Problem

Happy-path tests routinely miss off-by-one errors, buffer limits, or guardrail regressions. Without explicit boundary coverage, downstream invariants fail silently.

Solution

Adopt a deliberate boundary grid: below, at, and above each documented limit. Use param_test! to table-drive the grid and Chicago TDD Tools assertions such as assert_in_range! and assert_guard_constraint!. Include boundary-focused names so a failing case communicates which edge is breached.

Forces

  • Thoroughness vs. duplication: table-driven tests keep boundaries concise
  • Performance vs. realism: small data sets reproduce the failure quickly
  • Maintainability vs. specificity: boundary constants should live in one place

Examples

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;
use chicago_tdd_tools::validation::guards::{GuardValidator, MAX_RUN_LEN};

param_test! {
    #[case("below", MAX_RUN_LEN - 1, true)]
    #[case("at", MAX_RUN_LEN, true)]
    #[case("above", MAX_RUN_LEN + 1, false)]
    fn test_run_length_boundaries(label: &str, length: usize, expected_ok: bool) {
        let validator = GuardValidator::new();
        let result = validator.validate_run_length(length);
        match expected_ok {
            true => assert_ok!(&result, "{label} should be accepted"),
            false => assert_err!(&result, "{label} should be rejected"),
        }
    }
}
}
  • Pattern 2: Error Path Testing
  • Pattern 4: Resource Cleanup
  • Pattern 20: Macro Pattern Enforcement

Pattern 4: Resource Cleanup

Context

Your tests allocate resources – files, network ports, containers, telemetry backends – that must be released even when assertions fail.

Problem

Forgetting to release resources causes nondeterministic failures, leaking containers, or state pollution between tests. Manual cleanup logic scatters across tests and is easy to miss.

Solution

Use Chicago TDD Tools fixtures and the RAII guarantees they provide. Wrap resource management inside fixture_test! or fixture_test_with_timeout!, storing handles in the fixture. Allow Drop implementations and the framework cleanup to run automatically so every test returns to a known good state.

Forces

  • Determinism vs. speed: automated teardown must be reliable without slowing the hot path
  • Simplicity vs. observability: cleanup should be invisible unless a failure occurs
  • Isolation vs. reuse: fixtures create fresh state while sharing setup logic

Examples

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;
use testcontainers::clients::Cli as DockerCli;

struct DockerFixture {
    docker: DockerCli,
}

fixture_test!(test_exec_container_command, fixture, {
    // Arrange
    let docker = DockerCli::default();
    let container = docker.run("alpine:3.19");

    // Act
    let result = container.exec("echo", &["ok"])?;

    // Assert
    assert_eq!(result.stdout, "ok\n");

    Ok::<(), testcontainers::Error>(())
});
}

The fixture ensures containers stop even if the assertion fails.

  • Pattern 5: Real Collaborators
  • Pattern 16: Fixture Lifecycle Management
  • Pattern 18: Timeout Defense in Depth

Pattern 5: Real Collaborators

Context

You are validating behavior that depends on external systems – telemetry, containers, queues – and want confidence that integrations behave the same way in production.

Problem

Mock-heavy tests can mask integration gaps, drift from reality, and erode trust in the test suite. When production fails, tests offer little guidance.

Solution

Use the framework's integration helpers to exercise real collaborators. For containers, enable the testcontainers feature and run against Docker. For telemetry, use otel_test! and weaver_test! to validate spans and semantic conventions. Keep tests categorized so slower integration suites run intentionally.

Forces

  • Fidelity vs. speed: real dependencies cost more time, so isolate them behind feature flags and profiles
  • Determinism vs. variability: control randomness via fixtures and builders
  • Observability vs. complexity: prefer higher-level validators over low-level asserts

Examples

#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;
use chicago_tdd_tools::observability::otel::{OtelTestHelper, SpanValidator};

otel_test!(test_span_follows_conventions, {
    // Arrange
    let helper = OtelTestHelper::new();
    let span = helper.capture(|tracer| tracer.span("checkout"));

    // Act
    let result = helper.assert_spans_valid(&[span.clone()]);

    // Assert
    assert_ok!(&result);
});
}
#![allow(unused)]
fn main() {
use chicago_tdd_tools::prelude::*;
use chicago_tdd_tools::integration::testcontainers::ContainerClient;

fixture_test_with_timeout!(test_postgres_roundtrip, fixture, 30, {
    // Arrange
    let client = ContainerClient::for_image("postgres:16").await?;

    // Act
    let rows = client.query("SELECT 1").await?;

    // Assert
    assert_eq!(rows[0].get::<i32, _>(0), 1);

    Ok::<(), testcontainers::Error>(())
});
}
  • Pattern 4: Resource Cleanup
  • Pattern 16: Fixture Lifecycle Management
  • Pattern 18: Timeout Defense in Depth

Architecture Patterns

Chicago TDD Tools is deliberately layered so teams can extend the framework without rewriting core functionality. The architecture patterns describe how modules compose, how extensions fit, and how the framework keeps behavior predictable across crates.

Use these patterns when building custom tooling on top of the framework or when evaluating how to contribute new modules upstream.

Pattern 6: Generic Base Layer

Context

You want a reusable testing foundation that can support multiple domains without pulling in their dependencies.

Problem

If the base includes domain code, every consumer drags in unused dependencies, slowing builds and introducing coupling. Conversely, a minimal base risks missing essential primitives.

Solution

Keep the core crate focused on generic capabilities – fixtures, builders, assertions, macros, state tracking. Expose them through capability modules (core, testing, validation, observability, integration). Ensure modules depend only on the standard library and optional features. Domain-specific abstractions live in downstream crates that compose the base.

Forces

  • Reuse vs. specialization: core must be broadly useful without dictating domain models
  • Build time vs. flexibility: optional features load only when needed
  • Stability vs. growth: base APIs should be stable; extensions add behavior

Examples

#![allow(unused)]
fn main() {
// src/lib.rs
pub mod core;
pub mod testing;
pub mod validation;
pub mod observability;
pub mod integration;

pub use core::{fixture, builders, assertions};
}
  • Pattern 7: Extension Layer
  • Pattern 8: Composition Over Duplication
  • Pattern 10: Capability Grouping

Pattern 7: Extension Layer

Context

A product team needs domain-specific fixtures, builders, or assertions on top of Chicago TDD Tools.

Problem

Embedding domain logic inside the core crate makes it hard to evolve independently and risks breaking other users. Repeated copy-paste of generic primitives leads to drift.

Solution

Create an extension crate that depends on the core. Compose base fixtures inside your domain fixture, forwarding behavior while adding fields and helpers. Re-export the pieces your team should import. Treat the base crate as an 80% solution and layer the remaining 20% locally.

Forces

  • Encapsulation vs. reuse: domain modules wrap core primitives rather than modify them
  • Stability vs. iteration speed: upstream stays stable while the extension can iterate quickly
  • Discoverability vs. sprawl: re-export only what the domain needs

Examples

#![allow(unused)]
fn main() {
// workflow-fixture crate
use chicago_tdd_tools::core::fixture::TestFixture;

pub struct WorkflowFixture {
    base: TestFixture<()>,
    engine: WorkflowEngine,
}

impl WorkflowFixture {
    pub fn new(engine: WorkflowEngine) -> Self {
        let base = TestFixture::new().expect("fixture");
        Self { base, engine }
    }
}
}
  • Pattern 6: Generic Base Layer
  • Pattern 8: Composition Over Duplication
  • Pattern 9: Single Source of Truth

Pattern 8: Composition Over Duplication

Context

You are adding a feature to an extension crate and need functionality already available in the base layer.

Problem

Copying helpers or macros breaks the single source of truth. Over time the copies diverge, and bug fixes must be applied in multiple places.

Solution

Compose existing primitives. Wrap fixtures inside domain fixtures, embed builders into higher-level builders, and use assertion macros rather than writing bespoke checks. When missing functionality is truly generic, add it to the base crate instead of forking it downstream.

Forces

  • Launch speed vs. long-term maintenance: composition keeps future upgrades cheap
  • Ergonomics vs. explicitness: wrappers can augment APIs without obscuring the base
  • Ownership vs. contribution: contribute upstream when the behavior benefits all users

Examples

#![allow(unused)]
fn main() {
pub struct OrderBuilder {
    base: TestDataBuilder,
}

impl OrderBuilder {
    pub fn new() -> Self {
        Self { base: TestDataBuilder::new() }
    }

    pub fn with_amount(mut self, amount: u64) -> Self {
        self.base = self.base.with_var("amount", amount.to_string());
        self
    }

    pub fn build_json(self) -> serde_json::Value {
        self.base.build_json().expect("json")
    }
}
}
  • Pattern 6: Generic Base Layer
  • Pattern 7: Extension Layer
  • Pattern 9: Single Source of Truth

Pattern 9: Single Source of Truth

Context

Constants, toggle lists, and feature matrices are needed across modules and extensions.

Problem

Duplicating configuration (timeouts, guard limits, feature lists) invites drift. Teams change one copy and forget the rest, producing inconsistent behavior.

Solution

Centralize invariants inside the module that owns them and re-export when necessary. Examples include timeout constants in core::macros::test, guard limits in validation::guards, and feature combinations in Cargo.toml. Extensions read these definitions instead of defining their own copies.

Forces

  • Accessibility vs. encapsulation: invariants must be easy to import without exposing internals
  • Flexibility vs. safety: allow customization through builders or configuration rather than duplicating constants
  • Documentation vs. code: comments and docs should reference the single source, not restated values

Examples

#![allow(unused)]
fn main() {
// src/core/macros/test.rs
pub const DEFAULT_UNIT_TEST_TIMEOUT_SECONDS: u64 = 1;
pub const DEFAULT_INTEGRATION_TEST_TIMEOUT_SECONDS: u64 = 30;

// src/validation/guards.rs
pub const MAX_RUN_LEN: usize = 8;
}
  • Pattern 8: Composition Over Duplication
  • Pattern 10: Capability Grouping
  • Pattern 18: Timeout Defense in Depth

Pattern 10: Capability Grouping

Context

You are browsing the Chicago TDD Tools codebase or designing a new module. You need to know where functionality belongs and how to expose it.

Problem

Without a consistent module taxonomy, features surface haphazardly. Consumers struggle to find capabilities, and maintainers duplicate structure.

Solution

Group modules by capability: core for foundational primitives, testing for advanced techniques, validation for guardrails, observability for telemetry, and integration for external systems. Re-export each group at the crate root to support both granular and high-level imports. New modules join one of these groups or motivate a new, clearly named capability.

Forces

  • Discoverability vs. granularity: capability groups provide short import paths while preserving modularity
  • Stability vs. evolution: groups rarely change, making documentation and IDE tooling reliable
  • Compilation vs. optionality: feature flags enable or disable entire capability slices

Examples

#![allow(unused)]
fn main() {
// src/lib.rs
pub mod core;          // fixtures, builders, assertions, macros
pub mod testing;       // property testing, mutation testing, snapshots
pub mod validation;    // guards, coverage, performance
pub mod observability; // otel, weaver
pub mod integration;   // testcontainers
}
  • Pattern 6: Generic Base Layer
  • Pattern 9: Single Source of Truth
  • Pattern 19: Feature Gate Slices

Design Patterns

These patterns codify the type-level and zero-cost techniques that make Chicago TDD Tools safe and fast. They explain how the framework encodes invariants, leverages Rust's compiler, and prevents misuse through types.

Use them when extending the framework or designing downstream APIs that should feel at home in the ecosystem.

Pattern 11: Zero-Cost Abstractions

Context

You are designing APIs or extensions and need expressive abstractions without runtime overhead.

Problem

Runtime polymorphism or heap allocations can slow hot paths. Manual inlining or duplicated code sacrifices readability.

Solution

Lean on generics, const generics, and macros to express behavior that compiles down to the same machine code as bespoke implementations. Favor references over owned values and prefer stack allocation. When dynamic dispatch is required, isolate it behind narrow traits.

Forces

  • Expressiveness vs. performance: abstractions must be ergonomic without cost
  • Safety vs. control: compile-time guarantees should not block optimization
  • Maintainability vs. specialization: macros and generics prevent copy-paste

Examples

#![allow(unused)]
fn main() {
pub fn measure_ticks<F, T>(operation: F) -> (T, u64)
where
    F: FnOnce() -> T,
{
    // Generic function specialized per call site; no dynamic dispatch
    chicago_tdd_tools::validation::performance::measure_ticks(operation)
}
}
  • Pattern 12: Type Safety with GATs
  • Pattern 14: Compile-Time Validation
  • Pattern 20: Macro Pattern Enforcement

Pattern 12: Type Safety with GATs

Context

You provide fixtures or builders that return references tied to the fixture lifetime.

Problem

Without explicit lifetimes, consumers can hold onto references after cleanup, causing dangling pointers or logic bugs.

Solution

Use Generic Associated Types (GATs) to bind returned data to the fixture lifetime. In AsyncFixtureProvider, Fixture<'a> ensures the borrow cannot outlive the provider. Pair GATs with sealed traits to prevent downstream crates from violating invariants.

Forces

  • Safety vs. ergonomics: GATs constrain lifetimes but keep APIs pleasant
  • Extensibility vs. soundness: sealing the trait permits internal evolution while preserving invariants
  • Async vs. sync: async fixtures require lifetimes that sync code cannot express without GATs

Examples

#![allow(unused)]
fn main() {
pub trait AsyncFixtureProvider: private::Sealed {
    type Fixture<'a>: Send where Self: 'a;
    type Error: std::error::Error + Send + Sync + 'static;

    fn create_fixture<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<Self::Fixture<'a>, Self::Error>> + Send + 'a>>;
}
}
  • Pattern 13: Sealed Traits for API Safety
  • Pattern 16: Fixture Lifecycle Management
  • Pattern 17: Builder-Driven Test Data

Pattern 13: Sealed Traits for API Safety

Context

A trait defines extension points (fixtures, validators) but external implementations could break invariants.

Problem

If downstream crates implement the trait arbitrarily, the framework cannot guarantee lifecycle management or error semantics. Breaking changes become impossible.

Solution

Use the sealed trait pattern: define a private module with a Sealed trait implemented only within the crate, and require Sealed as a supertrait. Consumers can use the trait, but only framework-defined implementations exist.

Forces

  • Safety vs. openness: sealing protects invariants while letting users compose functionality
  • Flexibility vs. versioning: internal changes become possible without breaking downstream code
  • Testability vs. encapsulation: tests can still construct fixtures via provided builders

Examples

#![allow(unused)]
fn main() {
mod private {
    pub trait Sealed {}
}

pub trait AsyncFixtureProvider: private::Sealed {
    // ...
}
}
  • Pattern 12: Type Safety with GATs
  • Pattern 17: Builder-Driven Test Data
  • Pattern 20: Macro Pattern Enforcement

Pattern 14: Compile-Time Validation

Context

Invariants (tick budgets, guard limits, feature combinations) should fail fast during compilation.

Problem

Runtime checks add overhead and can be bypassed in rarely executed code paths. Missing an invariant leads to subtle production bugs.

Solution

Push validation to compile time with const generics, type-level markers, and const_assert!. When runtime validation is unavoidable, encapsulate it in constructors that return Result, making misuse impossible through types.

Forces

  • Safety vs. flexibility: some values remain runtime, but defaults should be encoded in types
  • Compile time vs. ergonomics: const generics expose parameters without macros
  • Diagnostics vs. noise: compile errors must explain the invariant succinctly

Examples

#![allow(unused)]
fn main() {
pub const fn assert_tick_budget(ticks: u64) {
    const_assert!(ticks <= 8);
}

pub struct SizeValidatedArray<const SIZE: usize, const MAX: usize> {
    data: [u8; SIZE],
    _marker: PhantomData<[u8; MAX]>,
}
}
  • Pattern 11: Zero-Cost Abstractions
  • Pattern 15: Type State Enforcement
  • Pattern 18: Timeout Defense in Depth

Pattern 15: Type State Enforcement

Context

An API has a prescribed call order (Arrange → Act → Assert) or requires configuration before use.

Problem

Runtime enforcement of order relies on documentation and can be bypassed, leading to inconsistent state and test flakiness.

Solution

Model the phases as distinct types and use PhantomData to encode the current phase. Methods consume self and return the next state, making it impossible to call methods out of order. Chicago TDD Tools uses this to enforce AAA semantics internally.

Forces

  • Safety vs. ergonomic: type transitions should read naturally without verbose syntax
  • Flexibility vs. constraints: provide escape hatches only when absolutely necessary
  • Zero-cost vs. clarity: type state should erase at compile time

Examples

#![allow(unused)]
fn main() {
pub struct TestState<Phase> {
    context: Context,
    _phase: PhantomData<Phase>,
}

impl TestState<Arrange> {
    pub fn act(self) -> TestState<Act> { /* ... */ }
}
}
  • Pattern 1: AAA Pattern
  • Pattern 11: Zero-Cost Abstractions
  • Pattern 14: Compile-Time Validation

New Patterns from Practice

The framework continues to evolve through real-world usage. This section captures patterns that emerged after building large Chicago TDD deployments. They complement the classical design patterns with pragmatic guidance on fixtures, timeouts, feature flags, and macro enforcement.

Pattern 16: Fixture Lifecycle Management

Context

Complex tests require deterministic setup and teardown of shared state: databases, telemetry, temporary directories.

Problem

Manual lifecycle logic is error-prone. Forgetting teardown causes cascading failures across tests. Async setup complicates matters further.

Solution

Wrap lifecycle responsibilities in TestFixture or AsyncFixtureManager. Use the fixture to hold handles and expose helper methods. Let Drop and the manager .teardown() guarantee cleanup. For async resources, implement AsyncFixtureProvider and return strongly typed handles.

Forces

  • Determinism vs. flexibility: fixtures must isolate state yet allow custom behavior per test
  • Async vs. sync complexity: asynchronous resources require explicit lifecycle boundaries
  • Performance vs. safety: reuse is tempting, but fresh fixtures avoid hidden coupling

Examples

#![allow(unused)]
fn main() {
struct DbProvider;

impl chicago_tdd_tools::core::async_fixture::private::Sealed for DbProvider {}

impl AsyncFixtureProvider for DbProvider {
    type Fixture<'a> = DatabaseHandle;
    type Error = DbError;

    fn create_fixture<'a>(&'a self) -> DbFuture<'a, DatabaseHandle> {
        Box::pin(async move { DatabaseHandle::connect().await })
    }
}

async_test!(test_query_latency, {
    let manager = AsyncFixtureManager::new(DbProvider);
    let handle = manager.setup().await?;
    // ...
    manager.teardown().await?;
    Ok(())
});
}
  • Pattern 4: Resource Cleanup
  • Pattern 12: Type Safety with GATs
  • Pattern 18: Timeout Defense in Depth

Pattern 17: Builder-Driven Test Data

Context

Domain objects require multiple fields or nested structures. Hand-building them in tests scatters intent and duplicates defaults.

Problem

Verbose setup obscures the behavior under test. When requirements change, hundreds of tests need updates.

Solution

Wrap TestDataBuilder (or create your own builder) to provide fluent helpers and sensible defaults. Expose domain-specific methods (with_customer_id, with_balance) and return JSON or HashMap structures ready for assertions. Builders live close to the domain, yet reuse the underlying generic builder to avoid duplication.

Forces

  • Expressiveness vs. coupling: builders should reflect domain language without leaking implementation details
  • Defaults vs. explicitness: provide safe defaults but allow overrides
  • Reuse vs. specialization: share base builder logic; extensions add convenience

Examples

#![allow(unused)]
fn main() {
pub struct CustomerBuilder {
    base: TestDataBuilder,
}

impl CustomerBuilder {
    pub fn new() -> Self {
        Self {
            base: TestDataBuilder::new()
                .with_var("status", "active"),
        }
    }

    pub fn with_id(mut self, id: &str) -> Self {
        self.base = self.base.with_var("customer_id", id.to_string());
        self
    }

    pub fn build(self) -> serde_json::Value {
        self.base.build_json().expect("valid json")
    }
}
}
  • Pattern 8: Composition Over Duplication
  • Pattern 11: Zero-Cost Abstractions
  • Pattern 19: Feature Gate Slices

Pattern 18: Timeout Defense in Depth

Context

Async tests interact with containers, networks, or external services. A hung future or stalled process could freeze the suite.

Problem

Single-layer timeouts fail silently when they reside in the wrong place. For example, process-level timeouts kill the entire run without explaining which test stalled.

Solution

Layer timeouts at three levels:

  1. Test-level (tokio::time::timeout inside macros) – fails the specific test with a clear message.
  2. Runner-level (cargo-nextest profiles) – applies SLA-based timeouts per profile.
  3. Process-level (timeout wrapper in Makefile.toml) – stops catastrophic hangs.

Expose constants for standard timeouts (DEFAULT_UNIT_TEST_TIMEOUT_SECONDS = 1, DEFAULT_INTEGRATION_TEST_TIMEOUT_SECONDS = 30) and use *_with_timeout! macros for slow scenarios.

Forces

  • Resilience vs. noise: timeouts must be strict enough to catch hangs but lenient for expected latency
  • Diagnostics vs. overhead: detailed error messages help triage without cluttering success paths
  • Configurability vs. consistency: shared constants keep expectations aligned

Examples

# .config/nextest.toml
[profile.default]
slow-timeout = { period = "1s", terminate-after = 1 }

[profile.integration]
slow-timeout = { period = "30s", terminate-after = 1 }
#![allow(unused)]
fn main() {
fixture_test_with_timeout!(test_container_warmup, fixture, DEFAULT_INTEGRATION_TEST_TIMEOUT_SECONDS, {
    // slow operation
    Ok(())
});
}
  • Pattern 4: Resource Cleanup
  • Pattern 9: Single Source of Truth
  • Pattern 20: Macro Pattern Enforcement

Pattern 19: Feature Gate Slices

Context

The framework offers advanced capabilities (property testing, mutation testing, testcontainers, OTEL) that not every project needs.

Problem

Enabling every feature increases compile times and pulls in heavy dependencies. Disabling a feature accidentally can break tests silently.

Solution

Group related features into named slices in Cargo.toml (e.g., testing-extras, observability-full). Document the slice and expose cfg-gated APIs accordingly. Tests and examples import the feature-specific modules only when the feature is active, keeping the base lean.

Forces

  • Modularity vs. convenience: slices reduce duplication but still allow fine-grained toggles
  • Discoverability vs. complexity: a small number of curated slices keeps onboarding simple
  • Compatibility vs. optionality: code must compile cleanly with features disabled

Examples

[features]
default = ["logging"]
testing-extras = ["property-testing", "snapshot-testing", "fake-data"]
observability-full = ["otel", "weaver"]
#![allow(unused)]
fn main() {
#[cfg(feature = "weaver")]
pub mod weaver;
}
  • Pattern 6: Generic Base Layer
  • Pattern 10: Capability Grouping
  • Pattern 20: Macro Pattern Enforcement

Pattern 20: Macro Pattern Enforcement

Context

You need consistent test structure and timeouts without repeating boilerplate or relying on discipline alone.

Problem

Developers forget to add timeouts, skip AAA comments, or mix direct #[test] usage with framework macros, leading to drift and inconsistent behavior.

Solution

Embed enforcement inside macros. test! injects the AAA skeleton, async_test! and fixture_test! wrap bodies with tokio::time::timeout, and weaver_test! requires the weaver feature. Each macro centralizes best practices so using it guarantees compliance.

Forces

  • Consistency vs. flexibility: macros enforce conventions while allowing custom logic inside
  • Zero cost vs. tooling: expansions must stay small and compile quickly
  • Guidance vs. noise: failures should point to the missing convention explicitly

Examples

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! async_test {
    ($name:ident, $body:block) => {
        $crate::async_test_with_timeout!($name, 1, $body);
    };
}
}
#![allow(unused)]
fn main() {
#[cfg(not(feature = "otel"))]
#[macro_export]
macro_rules! otel_test {
    ($($tt:tt)*) => {
        compile_error!("OTEL testing requires the 'otel' feature. Enable with: --features otel");
    };
}
}
  • Pattern 1: AAA Pattern
  • Pattern 18: Timeout Defense in Depth
  • Pattern 19: Feature Gate Slices