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.


Getting Started

Not sure where to start? Choose your path:

Your SituationGo ToTime
I need to solve a problemChoosing Your Pattern5 min
I want a quick overviewAll Patterns Reference10 min
I want to learn systematicallyChoose a learning sequence below60-120 min
I want a specific patternJump to pattern reference belowVaries

Learning Sequences

Learn patterns progressively through structured tutorials:

SequenceFocusTimeDifficulty
Testing PatternsHow to write better tests90 minBeginner
Architecture PatternsHow to organize code60 minIntermediate
Design PatternsType safety and optimization120 minAdvanced

Recommended: Follow in order (Testing → Architecture → Design).


Choosing Your Pattern

🗺️ NAVIGATION | Find the right pattern for your testing problem

This guide helps you choose the right pattern(s) based on your testing situation.


Quick Pattern Finder

What's your challenge? Find it in the table below:

Your ChallengePattern FamilyPattern NameGo To
How do I structure a test?TestingPattern 1: AAA PatternLearn
How do I test error cases?TestingPattern 2: Error Path TestingLearn
How do I test edge cases?TestingPattern 3: Boundary ConditionsLearn
How do I clean up resources?TestingPattern 4: Resource CleanupLearn
Should I use mocks?TestingPattern 5: Real CollaboratorsLearn
How do I organize my code?ArchitecturePattern 6: Generic Base LayerLearn
How do I extend without duplicating?ArchitecturePattern 7: Extension LayerLearn
How do I avoid code duplication?ArchitecturePattern 8: Composition Over DuplicationLearn
How do I avoid data inconsistency?ArchitecturePattern 9: Single Source of TruthLearn
How do I organize large modules?ArchitecturePattern 10: Capability GroupingLearn
How do I optimize for performance?DesignPattern 11: Zero-Cost AbstractionsLearn
How do I prevent type errors?DesignPattern 12: Type Safety with GATsLearn
How do I prevent API misuse?DesignPattern 13: Sealed TraitsLearn
How do I validate at compile-time?DesignPattern 14: Compile-Time ValidationLearn
How do I enforce state machines?DesignPattern 15: Type State EnforcementLearn
How do I manage fixture lifecycle?DesignPattern 16: Fixture Lifecycle ManagementLearn
How do I build test data easily?DesignPattern 17: Builder-Driven Test DataLearn
How do I prevent timeouts?DesignPattern 18: Timeout DefenseLearn
How do I manage feature flags?DesignPattern 19: Feature Gate SlicesLearn
How do I enforce patterns with macros?DesignPattern 20: Macro Pattern EnforcementLearn

By Category

Testing Patterns: "How Do I Write Better Tests?"

These patterns solve fundamental testing problems:

PatternProblemSolution
Pattern 1: AAATests are hard to readStructure: Arrange, Act, Assert
Pattern 2: Error PathsI don't test failuresTest both success and error cases
Pattern 3: BoundariesI miss edge casesSystematically test limits
Pattern 4: Resource CleanupTests leak resourcesAutomatic fixture cleanup
Pattern 5: Real CollaboratorsMocks hide integration bugsTest with real implementations

When to use: All the time. These are foundational.

Learning Path: Testing Patterns Learning Sequence


Architecture Patterns: "How Do I Organize Code?"

These patterns solve structural problems:

PatternProblemSolution
Pattern 6: Generic BaseCode is duplicateExtract generic abstractions
Pattern 7: Extension LayerI can't extend without modifyingAdd layers for extensions
Pattern 8: CompositionDRY violations everywhereCompose instead of duplicating
Pattern 9: Single SourceData gets out of syncOne canonical source of truth
Pattern 10: Capability GroupsModule is too largeOrganize by capability, not type

When to use: During architecture phase and refactoring.

Learning Path: Architecture Patterns Learning Sequence


Design Patterns: "How Do I Make Code Safer?"

These patterns solve design and safety problems:

PatternProblemSolution
Pattern 11: Zero-CostAbstractions are slowZero-cost abstractions via generics
Pattern 12: Type SafetyType errors at runtimeUse GATs for safety
Pattern 13: Sealed TraitsAPI is too easy to misuseSeal traits to prevent misuse
Pattern 14: Compile-TimeErrors caught at runtimeValidate at compile-time
Pattern 15: Type StateState machines are error-proneEncode states in types
Pattern 16: Fixture LifecycleTest setup is complexManage lifecycle with traits
Pattern 17: Builder Test DataBuilding test data is tediousFluent builders for test data
Pattern 18: Timeout DefenseTests hang foreverTimeout defense in depth
Pattern 19: Feature GatesFeature flags are unreliableGate slices across codebase
Pattern 20: Macro EnforcementPatterns are easy to violateUse macros to enforce patterns

When to use: During design and implementation.

Learning Path: Design Patterns Learning Sequence


Decision Trees

"I'm writing a test. Which pattern do I need?"

┌─ Start: Writing a test
│
├─ What am I testing?
│  ├─ Normal behavior ──→ Pattern 1: AAA Pattern
│  ├─ Error behavior ──→ Pattern 2: Error Path Testing
│  ├─ Edge cases ──→ Pattern 3: Boundary Conditions
│  └─ Setup/teardown ──→ Pattern 4: Resource Cleanup
│
├─ What should I test against?
│  ├─ Mock/fake ──→ Consider Pattern 5: Real Collaborators
│  └─ Real implementation ──→ Pattern 5: Real Collaborators ✓
│
└─ How do I build test data?
   └─ Complex data ──→ Pattern 17: Builder-Driven Test Data

"I'm designing an architecture. Which patterns apply?"

┌─ Start: Designing architecture
│
├─ How do I organize modules?
│  ├─ By type (models, handlers, etc.) ──→ Consider Pattern 10: Capability Groups
│  └─ By capability ──→ Pattern 10: Capability Groups ✓
│
├─ How do I reuse code?
│  ├─ Copy-paste ──→ NO! Use Pattern 8: Composition Over Duplication
│  └─ Abstract base ──→ Pattern 6: Generic Base Layer
│
├─ How do I extend without modifying?
│  └─ Pattern 7: Extension Layer
│
└─ Where is the source of truth?
   └─ Pattern 9: Single Source of Truth

"I'm designing APIs. Which patterns keep them safe?"

┌─ Start: Designing public API
│
├─ Can downstream code misuse my API?
│  └─ YES ──→ Pattern 13: Sealed Traits
│
├─ Should errors be compile-time or runtime?
│  ├─ Compile-time ──→ Pattern 14: Compile-Time Validation
│  └─ Runtime ──→ Less safe, but sometimes necessary
│
├─ Does state machine matter?
│  ├─ YES (auth states, connection states) ──→ Pattern 15: Type State Enforcement
│  └─ NO ──→ Continue
│
├─ Are lifetimes complex?
│  └─ YES ──→ Pattern 12: Type Safety with GATs
│
└─ Should this be in macros?
   └─ Pattern 20: Macro Pattern Enforcement

Learning by Difficulty

Beginner (Start Here)

  1. Pattern 1: AAA Pattern - Foundation
  2. Pattern 2: Error Path Testing - See what not to do
  3. Pattern 3: Boundary Conditions - Edge cases matter
  4. Pattern 4: Resource Cleanup - Don't leak resources
  5. Pattern 5: Real Collaborators - Test with real code

Time: ~3 hours | Content: Testing fundamentals

Intermediate (Build on Basics)

  1. Pattern 6: Generic Base Layer - Code organization
  2. Pattern 8: Composition Over Duplication - DRY principle
  3. Pattern 10: Capability Grouping - Module organization
  4. Pattern 17: Builder-Driven Test Data - Practical testing
  5. Pattern 14: Compile-Time Validation - Type safety

Time: ~4 hours | Content: Architecture and safety

Advanced (Master the Craft)

  1. Pattern 11: Zero-Cost Abstractions - Performance
  2. Pattern 12: Type Safety with GATs - Advanced types
  3. Pattern 15: Type State Enforcement - State machines
  4. Pattern 13: Sealed Traits - API design
  5. Pattern 18: Timeout Defense - Robustness

Time: ~5 hours | Content: Advanced design and optimization


Pattern Combination Guide

"I want to write production-quality tests"

Use these patterns together:

  1. Pattern 1: AAA - Structure your tests
  2. Pattern 2: Error Paths - Test failures
  3. Pattern 3: Boundaries - Test edge cases
  4. Pattern 4: Resource Cleanup - Clean automatically
  5. Pattern 5: Real Collaborators - Use real dependencies
  6. Pattern 17: Builder Test Data - Build complex test data

Expected outcome: Comprehensive, maintainable test suite

"I want to build a safe, extensible API"

Use these patterns together:

  1. Pattern 6: Generic Base - Reusable abstractions
  2. Pattern 7: Extension Layer - Allow extensibility
  3. Pattern 13: Sealed Traits - Prevent misuse
  4. Pattern 14: Compile-Time Validation - Validate early
  5. Pattern 15: Type State - Enforce state machines
  6. Pattern 20: Macro Enforcement - Enforce usage patterns

Expected outcome: Safe, extensible, hard-to-misuse API

"I want maximum performance"

Use these patterns together:

  1. Pattern 11: Zero-Cost Abstractions - Generic dispatch
  2. Pattern 12: Type Safety with GATs - Type-safe lifetimes
  3. Pattern 14: Compile-Time Validation - Zero runtime checks
  4. Pattern 8: Composition Over Duplication - Avoid copies

Expected outcome: Fast code with safety guarantees


FAQ

Q: How many patterns should I learn? A: Start with Testing Patterns (5), then Architecture (5) for production code. Advanced designers learn all 20.

Q: Do I need to learn them in order? A: No, but the beginner patterns are prerequisites for understanding advanced ones.

Q: Can I use just one pattern? A: Yes, but patterns work together. Combine related patterns for best results.

Q: Where do I go from here? A: Choose a Learning Sequence or pick a pattern you need right now.

Q: How do patterns relate to the application guide? A: Application guide shows how to apply patterns in practice. Cookbook explains why patterns exist. Use both together.


Next Steps

Choose your learning path:

Or jump directly to the pattern you need from the Quick Finder above.


Remember: Patterns work together. As you learn each one, you'll recognize them appearing in others. That's the power of a pattern language.

All Patterns: Quick Reference Card

📚 REFERENCE | All 20 patterns at a glance

Quick lookup for all patterns. Use this to find a pattern and jump to its full description.


Testing Patterns (5)

#PatternProblemSolutionLearn More
1AAA PatternTests are unreadableStructure into Arrange-Act-Assert
2Error Path TestingFailures aren't testedTest both success AND error paths
3Boundary ConditionsEdge cases are missedSystematically test limits
4Resource CleanupTests leak resourcesAutomatic fixture cleanup
5Real CollaboratorsMocks hide bugsTest with real implementations

Key: These 5 patterns are the foundation. Use them in every test.


Architecture Patterns (5)

#PatternProblemSolutionLearn More
6Generic Base LayerCode duplicationExtract generic abstractions
7Extension LayerCan't extend without modifyingAdd layers for safe extension
8Composition Over DuplicationDRY violationsCompose instead of copying
9Single Source of TruthData inconsistenciesOne canonical source
10Capability GroupingMonolithic modulesOrganize by capability

Key: These 5 patterns organize code structure. Use during architecture phase.


Design Patterns (10)

#PatternProblemSolutionLearn More
11Zero-Cost AbstractionsAbstractions are slowUse generics, compile away overhead
12Type Safety with GATsType errors at runtimeGeneric Associated Types
13Sealed TraitsAPI is too easy to misuseSeal traits to prevent misuse
14Compile-Time ValidationErrors caught at runtimeValidate during compilation
15Type State EnforcementState machines are error-proneEncode states in the type system
16Fixture LifecycleComplex test setupManage with sealed traits
17Builder-Driven Test DataBuilding test data is tediousFluent builders for data
18Timeout DefenseTests hang indefinitelyTimeout defense in depth
19Feature Gate SlicesFeature flags are unreliableSlice-based feature gating
20Macro Pattern EnforcementPatterns are easy to violateUse macros to enforce

Key: These 10 patterns provide safety, performance, and design tools. Use during implementation.


Pattern Organization

By Complexity (Learning Path)

Phase 1 - Foundation (Read First)

  1. Pattern 1: AAA Pattern
  2. Pattern 2: Error Path Testing
  3. Pattern 3: Boundary Conditions

Phase 2 - Production Ready (Read Next) 4. Pattern 4: Resource Cleanup 5. Pattern 5: Real Collaborators 6. Pattern 17: Builder-Driven Test Data

Phase 3 - Architecture (Advanced) 6. Pattern 8: Composition Over Duplication 7. Pattern 10: Capability Grouping 8. Pattern 9: Single Source of Truth

Phase 4 - Advanced Design (Mastery) 11. Pattern 11: Zero-Cost Abstractions 12. Pattern 13: Sealed Traits 13. Pattern 15: Type State Enforcement 14. Pattern 20: Macro Pattern Enforcement


By Category (Type System)

Testing Patterns: Patterns 1-5 Architecture Patterns: Patterns 6-10 Design Patterns: Patterns 11-20


By Problem Domain

Testing Problems: Patterns 1-5, 17 Code Organization: Patterns 6-10 Type Safety: Patterns 12, 14, 15 API Design: Patterns 13, 20 Performance: Pattern 11 Robustness: Pattern 18 Reliability: Pattern 19


How to Use This Card

  1. Find your problem in the Problem column
  2. See the solution in the Solution column
  3. Click Learn More to read the full pattern
  4. Bookmark the pattern for future reference

Want to...Go to...
Choose a patternChoosing Your Pattern
Learn testingTesting Learning Sequence
Learn architectureArchitecture Learning Sequence
Learn designDesign Learning Sequence
All 20 patternsThis page (you are here)

Pattern Dependencies

Some patterns build on others. Recommended learning order:

Pattern 1 (AAA)
  ├─→ Pattern 2 (Error Paths)
  ├─→ Pattern 3 (Boundaries)
  ├─→ Pattern 4 (Resource Cleanup)
  └─→ Pattern 5 (Real Collaborators)
       └─→ Pattern 17 (Builder Test Data)

Pattern 6 (Generic Base)
  └─→ Pattern 8 (Composition)
       └─→ Pattern 10 (Capability Groups)

Pattern 14 (Compile-Time)
  └─→ Pattern 15 (Type State)

Pattern 13 (Sealed Traits)
  └─→ Pattern 20 (Macro Enforcement)

Statistics

MetricValue
Total Patterns20
Testing Patterns5
Architecture Patterns5
Design Patterns10
Difficulty RangeBeginner → Advanced
Total Learning Time~10 hours
Estimated Implementation2-3 weeks

Pro Tips

💡 Tip 1: You don't need to learn all 20 patterns at once. Start with Testing (1-5), then add what you need.

💡 Tip 2: Patterns often appear in combinations. When you use Pattern 5 (Real Collaborators), you'll probably also use Pattern 17 (Builder Test Data).

💡 Tip 3: Look for patterns in the codebase you're reading. The more you see patterns, the better you'll understand them.

💡 Tip 4: Bookmark the Decision Guide. You'll return to it when solving problems.


Next: Choose your learning path or jump to a pattern you need right now!

Learning Testing Patterns: 90-Minute Mastery

🎓 TUTORIAL | Master the 5 fundamental testing patterns

This tutorial guides you through the 5 testing patterns in a natural progression. Each builds on the previous one.

Time: ~90 minutes | Difficulty: Beginner | Prerequisites: Basic Rust knowledge


Module Overview

PatternTimeFocus
Pattern 1: AAA15 minTest structure and readability
Pattern 2: Error Paths20 minTesting failures and error cases
Pattern 3: Boundaries15 minEdge cases and limits
Pattern 4: Resource Cleanup15 minAutomatic cleanup and fixtures
Pattern 5: Real Collaborators15 minTesting with real dependencies
Practice Exercises15 minApply what you've learned

Part 1: AAA Pattern (15 minutes)

Goal: Understand how to structure tests for readability

The Three Phases

Every test has three parts:

  1. Arrange: Set up the test data and environment
  2. Act: Execute the behavior you're testing
  3. Assert: Verify the result is correct

Example Structure

#![allow(unused)]
fn main() {
test!(test_name, {
    // Arrange: Set up
    let input = 42;

    // Act: Execute
    let result = process(input);

    // Assert: Verify
    assert_eq!(result, 84);
});
}

Key Points

Do: Label each phase with comments ✅ Do: Test one behavior per test ✅ Do: Make test names describe what they test

Don't: Mix phases together ❌ Don't: Put logic in assertions ❌ Don't: Test multiple behaviors in one test

Checkpoint Question

How would you structure a test that:

  1. Creates a user
  2. Verifies the user was created correctly

Answer: Create → Verify (Pattern 1: AAA structure)


Part 2: Error Path Testing (20 minutes)

Goal: Test both success AND failure cases

Why Error Testing Matters

Tests that only verify success hide bugs. Real systems fail. You must test how code behaves when things go wrong.

Success Path vs. Error Path

#![allow(unused)]
fn main() {
// Success path: Everything works
test!(test_add_user_success, {
    // Arrange
    let mut db = Database::new();

    // Act
    let result = db.add_user("alice@example.com");

    // Assert
    assert_ok!(&result);  // Should succeed
});

// Error path: Something goes wrong
test!(test_add_duplicate_user_fails, {
    // Arrange
    let mut db = Database::new();
    db.add_user("alice@example.com").ok();  // Add once

    // Act
    let result = db.add_user("alice@example.com");  // Try to add again

    // Assert
    assert_err!(&result);  // Should fail
});
}

What to Test

For each behavior, test:

  1. Normal case - Everything works perfectly
  2. Invalid input - Bad data
  3. Boundary case - Limits (empty, max size, etc.)
  4. Error case - Something fails
  5. Concurrent case (if applicable) - Multiple threads

Checkpoint Question

You have a function divide(a, b) that returns Result<i32, Error>.

What test cases should you write?

Answers:

  1. ✅ Normal: divide(10, 2)Ok(5)
  2. ✅ Error: divide(10, 0)Err(Division by zero)
  3. ✅ Boundary: Negative numbers? Large numbers?

Part 3: Boundary Conditions (15 minutes)

Goal: Systematically test edge cases

What Are Boundaries?

Boundaries are the limits where bugs often hide:

  • Empty collections (size 0)
  • Full collections (size = capacity)
  • Negative numbers
  • Maximum values
  • Null/None cases
  • First vs. Last elements

Boundary Testing Pattern

#![allow(unused)]
fn main() {
test!(test_boundaries, {
    // Arrange
    let mut list = List::new();

    // Test: Empty list (boundary)
    assert_eq!(list.len(), 0);

    // Test: Add one (crossing boundary)
    list.add(42);
    assert_eq!(list.len(), 1);

    // Test: Add many (stress)
    for i in 0..1000 {
        list.add(i);
    }
    assert_eq!(list.len(), 1001);
});
}

Common Boundaries to Test

BoundaryTest ValuesExample
Empty0 itemsvec![]
Single1 itemvec![1]
Multiple2-many itemsvec![1, 2, 3...]
NegativeNegative numbers-1, -100
MaximumMax valueu32::MAX
MinimumMin valueu32::MIN

Checkpoint Question

You're testing a split_string(s, limit) function.

What boundary cases should you test?

Answers:

  1. ✅ Empty string
  2. ✅ Limit = 0
  3. ✅ Limit = 1
  4. ✅ Limit > length
  5. ✅ Very long string

Part 4: Resource Cleanup (15 minutes)

Goal: Ensure tests clean up automatically

Why Cleanup Matters

Tests often create resources:

  • Files
  • Database connections
  • Network sockets
  • Memory allocations

If not cleaned up, tests leak resources and fail.

The Fixture Pattern

Chicago TDD provides TestFixture for automatic cleanup:

#![allow(unused)]
fn main() {
test!(test_with_cleanup, {
    // Create fixture - sets up
    let fixture = TestFixture::new()?;

    // Use the fixture
    fixture.set_metadata("key", "value");

    // No need to cleanup - happens automatically
    // when fixture is dropped!
});  // Cleanup happens here automatically
}

What Gets Cleaned Up

  • File handles closed
  • Connections closed
  • Memory freed
  • Temporary directories removed

All automatic! ✅

Checkpoint Question

You're writing tests that use a database. What should you do?

Answer: Use a TestFixture that:

  • Connects to test database on creation
  • Runs tests
  • Closes connection automatically on drop

Part 5: Real Collaborators (15 minutes)

Goal: Test with real implementations, not mocks

Mocks vs. Real Implementations

#![allow(unused)]
fn main() {
// ❌ Using a mock (hides integration bugs)
test!(test_with_mock, {
    let mock_client = MockApiClient::new();  // Fake
    mock_client.set_response(Ok(User { ... }));
    let result = my_service.fetch_user(&mock_client);
    assert_ok!(&result);
    // But does real client work? Who knows!
});

// ✅ Using real implementation (catches real bugs)
test!(test_with_real_client, {
    let real_client = RealApiClient::new();  // Actually calls API
    let result = my_service.fetch_user(&real_client);
    assert_ok!(&result);
    // Proves it works with the real API
});
}

When to Use Real Collaborators

Always preferred:

  • Database queries
  • File I/O
  • Web services
  • Network calls

When it's fast (< 100ms):

  • Real implementations

Only when necessary:

  • Slow external services (use test doubles)
  • Non-deterministic behavior
  • Expensive operations

The Philosophy

Chicago TDD principle: Test behavior with real dependencies. This proves code actually works, not just in the test's imagination.

Checkpoint Question

You're testing a payment service. What should you test against?

Answer: A real (test) payment processor, not a mock. Why? Because the real one is what you'll use in production.


Putting It Together: Practice Exercise (15 minutes)

Exercise: User Registration Service

Write tests for this service:

#![allow(unused)]
fn main() {
pub struct UserService {
    db: UserDatabase,
}

impl UserService {
    pub fn register(&mut self, email: &str, password: &str) -> Result<User, Error> {
        // Validate input
        if email.is_empty() || password.len() < 8 {
            return Err(Error::InvalidInput);
        }

        // Check if user exists
        if self.db.user_exists(email) {
            return Err(Error::UserExists);
        }

        // Create user
        let user = User::new(email, password);
        self.db.save(&user)?;
        Ok(user)
    }
}
}

What Tests Should You Write?

Using all 5 patterns, write tests for:

  1. Pattern 1 (AAA): Structure tests clearly
  2. Pattern 2 (Error Paths):
    • Empty email ❌
    • Short password ❌
    • User already exists ❌
    • Valid registration ✅
  3. Pattern 3 (Boundaries):
    • Minimum password length (7, 8, 9 chars)
    • Very long email
  4. Pattern 4 (Resource Cleanup):
    • Use fixture for database cleanup
  5. Pattern 5 (Real Collaborators):
    • Use real UserDatabase, not mock

Example Solution Framework

#![allow(unused)]
fn main() {
test!(test_register_success, {
    // Arrange
    let fixture = TestFixture::new()?;
    let db = fixture.test_database();  // Real database
    let mut service = UserService::new(db);

    // Act
    let result = service.register("alice@example.com", "password123");

    // Assert
    assert_ok!(&result);
    let user = result.unwrap();
    assert_eq!(user.email, "alice@example.com");
});
}

Summary: The 5 Testing Patterns

PatternGoalUse When
Pattern 1: AAARead­able testsEvery test
Pattern 2: Error PathsTest failuresEvery function
Pattern 3: BoundariesTest limitsEvery input
Pattern 4: Resource CleanupAuto cleanupComplex tests
Pattern 5: Real CollaboratorsReal depsIntegration tests

Next Steps

Immediate (Today)

Write 5-10 tests using all patterns for something in your codebase.

Short-term (This Week)

Review existing tests. Rewrite any that don't follow these patterns.

Long-term (This Month)

Learn Architecture Patterns to organize your code structure.


Checkpoint: Do You Know...?

  • How to structure a test with AAA?
  • How to test both success and error cases?
  • What boundary conditions to test?
  • How fixtures provide automatic cleanup?
  • Why real collaborators are better than mocks?

If you answered yes to all, you've mastered Testing Patterns! 🎉


Resources


Congratulations! You now understand the 5 fundamental testing patterns. Next, learn how to organize your code with Architecture Patterns.

Learning Architecture Patterns: 60-Minute Mastery

🎓 TUTORIAL | Master the 5 fundamental architecture patterns

This tutorial guides you through the 5 architecture patterns that organize your codebase.

Time: ~60 minutes | Difficulty: Intermediate | Prerequisites: Testing Patterns


Module Overview

PatternTimeFocus
Pattern 6: Generic Base Layer15 minEliminating duplication with abstractions
Pattern 7: Extension Layer12 minSafe extensibility without modification
Pattern 8: Composition Over Duplication12 minDRY principle in action
Pattern 9: Single Source of Truth12 minKeeping data consistent
Pattern 10: Capability Grouping9 minOrganizing large modules

Part 1: Generic Base Layer (15 minutes)

Goal: Eliminate code duplication through generic abstractions

The Problem: Code Duplication

Imagine you have multiple types that behave similarly:

#![allow(unused)]
fn main() {
// ❌ Duplicate code everywhere
struct FileStorage {
    path: String,
}

impl FileStorage {
    fn get(&self, key: &str) -> Result<String> { ... }
    fn set(&self, key: &str, value: String) -> Result<()> { ... }
    fn delete(&self, key: &str) -> Result<()> { ... }
}

struct DatabaseStorage {
    connection: Connection,
}

impl DatabaseStorage {
    fn get(&self, key: &str) -> Result<String> { ... }
    fn set(&self, key: &str, value: String) -> Result<()> { ... }
    fn delete(&self, key: &str) -> Result<()> { ... }
}

// Duplicated 9 times!
}

The Solution: Generic Abstraction

#![allow(unused)]
fn main() {
// ✅ Define generic behavior once
pub trait KeyValueStore {
    fn get(&self, key: &str) -> Result<String>;
    fn set(&self, key: &str, value: String) -> Result<()>;
    fn delete(&self, key: &str) -> Result<()>;
}

// Both implement the same trait
struct FileStorage { ... }
impl KeyValueStore for FileStorage { ... }

struct DatabaseStorage { ... }
impl KeyValueStore for DatabaseStorage { ... }

// Code that works with BOTH
fn backup_all_data(store: &dyn KeyValueStore) {
    // Works with file, database, or any implementation!
}
}

When to Use This Pattern

Use when: You have similar code in multiple places ✅ Use when: You want to swap implementations ✅ Use when: You want to test with fakes/stubs

Checkpoint Question

You have RedisCache and MemoryCache with nearly identical code.

What should you do?

Answer: Extract a Cache trait and implement it for both.


Part 2: Extension Layer (12 minutes)

Goal: Allow safe extensions without modifying existing code

The Problem: Modifying Core Code

#![allow(unused)]
fn main() {
// Original code
pub struct HttpServer {
    fn handle_request(&self, req: Request) {
        // Handle request
    }
}

// ❌ To add logging, you modify the core:
impl HttpServer {
    fn handle_request(&self, req: Request) {
        println!("Request: {:?}", req);  // Logging added
        // Handle request
    }
}

// ❌ To add authentication, you modify again:
impl HttpServer {
    fn handle_request(&self, req: Request) {
        println!("Request: {:?}", req);  // Still here
        // Check auth
        // Handle request
    }
}

// ❌ Code gets messy fast!
}

The Solution: Extension Layer

#![allow(unused)]
fn main() {
// Core code - never changes
pub struct HttpServer { ... }

// Extension layer - add features here
pub struct LoggingHttpServer {
    inner: HttpServer,
}

impl LoggingHttpServer {
    fn handle_request(&self, req: Request) {
        println!("Request: {:?}", req);
        self.inner.handle_request(req);
    }
}

// Another extension layer
pub struct AuthHttpServer {
    inner: LoggingHttpServer,
}

impl AuthHttpServer {
    fn handle_request(&self, req: Request) {
        if !req.is_authenticated() {
            return Err(Unauthorized);
        }
        self.inner.handle_request(req);
    }
}

// Usage: Stack them!
let server = HttpServer::new();
let logged = LoggingHttpServer::new(server);
let secured = AuthHttpServer::new(logged);
}

Why This Matters

  • Original code never changes → No bugs introduced
  • Easy to test → Test each layer separately
  • Composable → Mix and match features
  • Reversible → Remove a layer anytime

Checkpoint Question

You need to add timeout handling to a database connection.

Using the Extension Layer pattern, how would you do it?

Answer: Create a TimeoutConnection wrapper that wraps the original Connection.


Part 3: Composition Over Duplication (12 minutes)

Goal: Use composition instead of copying code

The Problem: Copy-Paste Duplication

#![allow(unused)]
fn main() {
// ❌ Copy-pasting code
struct Logger {
    buffer: Vec<String>,
}

impl Logger {
    fn log(&mut self, msg: &str) {
        self.buffer.push(msg.to_string());
    }
}

struct FileWriter {
    buffer: Vec<String>,  // DUPLICATE: Same buffer
    path: String,
}

impl FileWriter {
    fn write(&mut self, data: &str) {
        self.buffer.push(data.to_string());  // DUPLICATE: Same logic
    }
}

// Now you need to fix a bug in the buffer logic...
// You have to fix it in TWO places! 😞
}

The Solution: Composition

#![allow(unused)]
fn main() {
// ✅ Create a shared component
pub struct StringBuffer {
    data: Vec<String>,
}

impl StringBuffer {
    fn append(&mut self, s: &str) {
        self.data.push(s.to_string());
    }
}

// Compose it into both
struct Logger {
    buffer: StringBuffer,
}

struct FileWriter {
    buffer: StringBuffer,  // Same component
    path: String,
}

// Now: Fix bug in StringBuffer once, both are fixed! ✅
}

Composition vs. Inheritance

AspectCompositionInheritance
Reusability✅ Mix and match❌ Rigid hierarchy
Maintainability✅ Changes in one place❌ Changes everywhere
Flexibility✅ Swap parts easily❌ Can't swap
Testability✅ Test component alone❌ Test whole tree

Rule of thumb: If you're copy-pasting code, use composition instead.

Checkpoint Question

You have JsonParser and XmlParser with 50 lines of duplicate validation code.

What should you do?

Answer: Extract a Validator component and use it in both.


Part 4: Single Source of Truth (12 minutes)

Goal: Keep data consistent by having one canonical source

The Problem: Data Inconsistency

#![allow(unused)]
fn main() {
// ❌ Multiple copies of the same data
struct UserCache {
    users: HashMap<u32, User>,  // Copy of database
}

struct UserService {
    cache: UserCache,
    database: Database,  // Original
}

// Update database
service.database.update_user(123, new_data)?;

// Oops! Cache is now stale
// service.cache.users[&123] is out of date!

// Different code paths get different data 😞
}

The Solution: One Source of Truth

#![allow(unused)]
fn main() {
// ✅ Truth in one place
struct UserService {
    database: Database,  // Only source of truth
}

// Cache is derived from database
fn get_user(&self, id: u32) -> Result<User> {
    // Get from database (single source)
    self.database.fetch(id)
}

// Update
fn update_user(&mut self, id: u32, data: UserUpdate) -> Result<()> {
    // Update only the source
    self.database.update(id, data)
}

// Always consistent! ✅
}

When to Apply

Apply when: Data appears in multiple places ✅ Apply when: Synchronization is complex ✅ Apply when: Consistency matters (payment systems, authorization, etc.)

Examples

DomainTruthDerived
E-commerceDatabase (orders)Cache, indices, reports
AuthDatabase (permissions)Session tokens, caches
AnalyticsRaw eventsAggregations, reports

Checkpoint Question

You have user data in both database and cache.

Which is the single source of truth?

Answer: Database. Cache is derived/cached from it. Update database first, invalidate cache.


Part 5: Capability Grouping (9 minutes)

Goal: Organize large modules by capability, not by type

The Problem: Type-Based Organization

project/
├── models/          # All data types
│   ├── user.rs
│   ├── order.rs
│   └── payment.rs
├── handlers/        # All HTTP handlers
│   ├── user_handler.rs
│   ├── order_handler.rs
│   └── payment_handler.rs
├── persistence/     # All database code
│   ├── user_repo.rs
│   ├── order_repo.rs
│   └── payment_repo.rs

// ❌ To understand "user" feature, you jump between 4 files!
// ❌ Each file is small but scattered
// ❌ Hard to find related code

The Solution: Capability-Based Organization

project/
├── users/               # Everything for users
│   ├── model.rs
│   ├── handler.rs
│   ├── repository.rs
│   └── tests.rs
├── orders/              # Everything for orders
│   ├── model.rs
│   ├── handler.rs
│   ├── repository.rs
│   └── tests.rs
├── payments/            # Everything for payments
│   ├── model.rs
│   ├── handler.rs
│   ├── repository.rs
│   └── tests.rs

// ✅ To understand "users", all code is in `users/`!
// ✅ Related code is together
// ✅ Easy to find dependencies

Benefits

  • Cohesion: Related code together
  • Discoverability: Find code easily
  • Modularity: Move features together
  • Testability: Test capability in one place

Checkpoint Question

Your project has models/, handlers/, and db/ directories.

How would you reorganize for better capability grouping?

Answer: Create users/, orders/, products/ directories, each containing models, handlers, and db code.


Putting It Together: Complete Exercise (15 minutes)

Exercise: Building an Order Processing System

You're building an order system with these requirements:

  1. Accept orders
  2. Store them in database
  3. Can switch between PostgreSQL and SQLite
  4. Add logging without modifying core code
  5. Keep inventory count accurate
  6. Organize code clearly

Design Using All 5 Patterns

Pattern 6 (Generic Base): Create Storage trait

#![allow(unused)]
fn main() {
trait OrderStorage {
    fn save(&mut self, order: &Order) -> Result<()>;
    fn get(&self, id: u32) -> Result<Order>;
}

// Both implementations
struct PostgresStorage { ... }
struct SqliteStorage { ... }
}

Pattern 7 (Extension Layer): Add logging

#![allow(unused)]
fn main() {
struct LoggingStorage {
    inner: Box<dyn OrderStorage>,
}
}

Pattern 8 (Composition): Shared validation

#![allow(unused)]
fn main() {
struct OrderValidator { ... }
struct Order Service {
    storage: Box<dyn OrderStorage>,
    validator: OrderValidator,  // Composed
}
}

Pattern 9 (Single Source): Inventory truth

#![allow(unused)]
fn main() {
struct InventoryService {
    database: Database,  // Single source
    // NOT: inventory_cache
}
}

Pattern 10 (Capability): Organize by feature

order_service/
├── model.rs
├── handler.rs
├── storage.rs
├── validator.rs
└── tests.rs

Summary: The 5 Architecture Patterns

PatternProblemSolution
6: Generic BaseDuplicationTraits and abstraction
7: Extension LayerModificationWrapping instead of changing
8: CompositionCopy-pasteShare components
9: Single SourceInconsistencyOne source of truth
10: CapabilityDisorganizationGroup by feature

Next Steps

Apply These Patterns

Choose one pattern and apply it to your current project this week.

Learn Design Patterns

Once you master architecture, learn Design Patterns for type safety and optimization.


Checkpoint: Do You Know...?

  • How to use traits to eliminate duplication?
  • How to extend code without modifying it?
  • When to use composition over inheritance?
  • How to keep data consistent?
  • How to organize code by capability?

If yes to all, you've mastered Architecture Patterns! 🎉


Congratulations! You now understand how to organize production code. Next, master type safety and design with Design Patterns.

Learning Design Patterns: 120-Minute Mastery

🎓 TUTORIAL | Master the 10 advanced design patterns

This tutorial guides you through the 10 design patterns that create safe, fast, maintainable systems.

Time: ~120 minutes | Difficulty: Advanced | Prerequisites: Testing + Architecture


Module Overview

PatternTimeFocus
Pattern 11: Zero-Cost Abstractions12 minPerformance without sacrifice
Pattern 12: Type Safety with GATs12 minLifetimes and type safety
Pattern 13: Sealed Traits10 minPrevent API misuse
Pattern 14: Compile-Time Validation12 minMove errors left
Pattern 15: Type State Enforcement12 minEncode state in types
Pattern 16: Fixture Lifecycle12 minSafe resource management
Pattern 17: Builder-Driven Test Data12 minFluent test builders
Pattern 18: Timeout Defense12 minPrevent hangs
Pattern 19: Feature Gate Slices12 minReliable feature flags
Pattern 20: Macro Pattern Enforcement12 minEnforce patterns with code

Part 1: Zero-Cost Abstractions (12 minutes)

Goal: Performance through generics and compile-time specialization

The Problem: Abstraction vs. Performance

#![allow(unused)]
fn main() {
// ❌ Abstraction adds runtime cost
fn process_data(data: &dyn Iterator<Item = i32>) {
    // Dynamic dispatch = function pointers = overhead
    for item in data {
        // ...
    }
}

// Result: Slower than necessary
}

The Solution: Generics = Zero Cost

#![allow(unused)]
fn main() {
// ✅ Generic = compile specialization
fn process_data<I: Iterator<Item = i32>>(data: I) {
    // Compiler generates specialized code for EACH type
    // No function pointers, no indirection
    for item in data {
        // ...
    }
}

// Rust compiles:
// - process_data::<Vec<i32>::IntoIter>
// - process_data::<ArrayIter>
// - process_data::<CustomIterator>
// Each one optimized for its type!
}

Key Principle

Rust compiler can monomorphize generics - it creates specialized versions for each type. This costs compilation time but zero runtime cost.

When to Use Generics vs. Trait Objects

ChoiceCostUse When
GenericsCompile-timeSize known, type fixed
Trait objectsRuntimeSize unknown, type varies
#![allow(unused)]
fn main() {
// ✅ Generics - no runtime cost
fn sort<T: Ord>(list: &mut [T]) { ... }

// ✅ Trait objects - when you need dynamic dispatch
fn apply_filter(items: &[i32], filter: &dyn Fn(i32) -> bool) { ... }
}

Checkpoint Question

You have a function that processes different collection types.

Should you use generics or trait objects?

Answer: Generics. Let compiler specialize for each type.


Part 2: Type Safety with GATs (12 minutes)

Goal: Use Generic Associated Types to prevent lifetime bugs

The Problem: Complex Lifetimes

#![allow(unused)]
fn main() {
// ❌ Lifetime issues with references
trait DataProvider {
    fn get(&self) -> &str;
}

// Can this reference outlive the provider?
// The compiler can't tell!
}

The Solution: GATs

#![allow(unused)]
fn main() {
// ✅ GATs bind lifetime to self
trait DataProvider {
    type Data<'a> where Self: 'a;

    fn get(&'a self) -> Self::Data<'a>;
}

// Now compiler KNOWS: returned data is tied to self's lifetime
}

Why This Matters

#![allow(unused)]
fn main() {
// With GATs, this is impossible:
fn use_provider(provider: &Provider) {
    let data = provider.get();
    drop(provider);  // ❌ Compiler error!
    println!("{:?}", data);  // data would be dangling
}

// Without GATs, it might compile (dangerous!)
}

When to Use GATs

Use when: Returning references from traits ✅ Use when: Fixture providers with borrowed data ✅ Use when: APIs that care about lifetimes

Checkpoint Question

You have a trait that returns a reference to internal data.

How do you ensure it can't outlive the source?

Answer: Use GATs to bind the returned reference lifetime to self.


Part 3: Sealed Traits (10 minutes)

Goal: Prevent external implementations of your traits

The Problem: API Misuse

#![allow(unused)]
fn main() {
// ❌ Public trait - anyone can implement!
pub trait Serializable {
    fn serialize(&self) -> String;
}

// External code implements it wrongly:
impl Serializable for String {
    fn serialize(&self) -> String {
        "nope".to_string()  // Wrong!
    }
}

// Your code breaks!
}

The Solution: Sealed Traits

#![allow(unused)]
fn main() {
// ✅ Private sealing module
mod sealed {
    pub trait Sealed {}
}

pub trait Serializable: sealed::Sealed {
    fn serialize(&self) -> String;
}

// Only WE can implement Sealed
impl sealed::Sealed for MyType {}
impl Serializable for MyType { ... }

// External code CANNOT implement Serializable
// because they can't implement sealed::Sealed!
}

Why This Matters

  • Prevents misuse → Forced to use correct implementations
  • API evolution → Can change internals safely
  • Documentation → Readers know this is sealed

Pattern

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

pub trait PublicTrait: sealed::Sealed {
    fn public_method(&self);
}

// Only internal types can implement
pub struct InternalType;
impl sealed::Sealed for InternalType {}
impl PublicTrait for InternalType { ... }
}

Checkpoint Question

You have an important trait that must have specific implementations.

How do you prevent user code from breaking it?

Answer: Seal the trait so only your code can implement it.


Part 4: Compile-Time Validation (12 minutes)

Goal: Catch errors during compilation, not at runtime

The Problem: Runtime Errors

#![allow(unused)]
fn main() {
// ❌ Errors at runtime
fn process(config: &str) -> Result<Data, Error> {
    let parsed = parse_config(config)?;  // Might fail at runtime
    validate_config(parsed)?;  // Another runtime check
    Ok(build_data(parsed))
}

// Errors are found after deployment 😞
}

The Solution: Phantom Types

#![allow(unused)]
fn main() {
// ✅ Errors at compile time
struct Config<S> {
    data: String,
    _state: PhantomData<S>,
}

// Parsing: Raw → Parsed
impl Config<Raw> {
    fn parse(s: &str) -> Result<Config<Parsed>, Error> {
        let data = validate(s)?;
        Ok(Config { data, _state: PhantomData })
    }
}

// Building: Only works with Parsed
impl Config<Parsed> {
    fn build(self) -> Data {
        // Can only call this on Parsed, never Raw
        Data::new(&self.data)
    }
}

// ✅ This is impossible:
let raw = Config::<Raw>::from("config");
raw.build();  // ❌ Compiler error! Use .parse() first
}

Key Idea

Encode requirements in types. Make it impossible to violate at compile-time.

Common Examples

PatternTypesPurpose
Builder patternRaw → BuiltEnsure all fields set
State machinesIdle → RunningPrevent invalid operations
Type tokensFoo<_> with phantomTrack type information

Checkpoint Question

You have an API that requires:

  1. Initialize
  2. Configure
  3. Start

How do you prevent wrong order?

Answer: Use phantom types to track state:

  • Service<Uninitialized>.init()Service<Initialized>
  • Service<Initialized>.configure()Service<Configured>
  • Service<Configured>.start()Service<Running>

Part 5: Type State Enforcement (12 minutes)

Goal: Use the type system to enforce valid state transitions

The Problem: Invalid States

#![allow(unused)]
fn main() {
// ❌ Can access before initialization
struct Connection {
    socket: Option<Socket>,  // None until connected
}

impl Connection {
    fn send(&mut self, data: &[u8]) -> Result<()> {
        match self.socket {
            Some(ref mut s) => s.write(data),
            None => Err("Not connected"),  // Runtime error!
        }
    }
}

// Code can call send() before connect() - runtime error!
}

The Solution: Type State

#![allow(unused)]
fn main() {
// ✅ Types enforce state
struct Connection<State> { ... }

// States
pub struct Disconnected;
pub struct Connected;

// Only Disconnected can connect
impl Connection<Disconnected> {
    fn connect(mut self, addr: &str) -> Result<Connection<Connected>> {
        self.socket = Some(Socket::new(addr)?);
        Ok(Connection { ... })
    }
}

// Only Connected can send
impl Connection<Connected> {
    fn send(&mut self, data: &[u8]) -> Result<()> {
        self.socket.write(data)?;  // socket is guaranteed Some
        Ok(())
    }
}

// ✅ Impossible to call send() before connect():
let conn = Connection::new();  // Disconnected
conn.send(b"data")?;  // ❌ Compiler error!

let conn = conn.connect("127.0.0.1")?;  // Now Connected
conn.send(b"data")?;  // ✅ Allowed!
}

Benefits

  • Compile-time safety → No panics from invalid states
  • No runtime checks → No Option/Result overhead
  • Clear API → Code documents valid transitions
  • Impossible states → Some states just can't happen

Checkpoint Question

You have a database transaction that must:

  1. Begin
  2. Execute
  3. Commit/Rollback

How do you prevent calling Commit before Begin?

Answer: Use type states:

  • Tx<NotStarted>.begin()Tx<Started>
  • Tx<Started>.commit()Tx<Finished>

Part 6-10: Summary of Advanced Patterns

The remaining 5 patterns build on what you've learned:

#PatternWhat It Does
16Fixture LifecycleManage resources with sealed traits (combines patterns 13+2)
17Builder Test DataFluent builders for test setup (applies compilation validation)
18Timeout DefenseMultiple timeout strategies (zero-cost + compile-time)
19Feature GatesType-safe feature flags (type state + sealed traits)
20Macro EnforcementCompile-time pattern checks via macros (ultimate compile-time validation)

Putting It Together: Complete Design

Exercise: Build a Safe Transaction System

Requirements:

  1. Transactions must go: Begin → Execute → Commit
  2. Can't add statements after commit
  3. Type-safe with zero runtime overhead
  4. Can't be misused by users
  5. Test data builder for testing

Solution Using All Patterns

#![allow(unused)]
fn main() {
// Pattern 15 + 14: Type state
pub struct Transaction<State> {
    id: u32,
    statements: Vec<String>,
    _state: PhantomData<State>,
}

pub struct NotStarted;
pub struct Started;
pub struct Committed;

// Pattern 14 + 15: Compile-time validation
impl Transaction<NotStarted> {
    pub fn begin() -> Transaction<Started> {
        Transaction { ... }
    }
}

impl Transaction<Started> {
    pub fn add_statement(&mut self, sql: &str) -> Result<()> {
        self.statements.push(sql.to_string());
        Ok(())
    }

    pub fn commit(self) -> Transaction<Committed> {
        // Execute all statements
        Transaction { ... }
    }
}

// Pattern 13: Sealed to prevent misuse
mod sealed {
    pub trait Sealed {}
}

pub trait TransactionOps: sealed::Sealed {
    fn execute(&self) -> Result<()>;
}

impl sealed::Sealed for Transaction<Started> {}
impl TransactionOps for Transaction<Started> {
    fn execute(&self) -> Result<()> { ... }
}

// Pattern 17: Builder for tests
pub struct TransactionBuilder {
    statements: Vec<String>,
}

impl TransactionBuilder {
    pub fn new() -> Self { ... }
    pub fn add(mut self, sql: &str) -> Self {
        self.statements.push(sql.to_string());
        self
    }
    pub fn build(self) -> Transaction<Started> { ... }
}

// ✅ Usage is type-safe:
let mut tx = Transaction::begin();
tx.add_statement("INSERT ...")?;
let tx = tx.commit();  // Type changes to Committed

// ❌ Impossible:
let tx = Transaction::begin();
let tx = tx.commit();
tx.add_statement("SELECT");  // ❌ Compiler error! tx is Committed
}

Summary: The 10 Design Patterns

PatternGoalMechanism
11Zero-costGenerics + monomorphization
12LifetimesGATs (Generic Associated Types)
13API safetySealed traits
14ValidationPhantom types + state
15State machinesType states
16ResourcesLifecycle traits
17Test dataFluent builders
18RobustnessTimeout strategies
19FeaturesType-safe gates
20EnforcementProcedural macros

Next Steps

Master One Pattern Per Week

  1. Week 1: Zero-Cost & GATs
  2. Week 2: Sealed & Compile-Time
  3. Week 3: Type State & Lifecycle
  4. Week 4: Builder, Timeout, Features, Macros

Apply to Your Project

Choose the top 3 patterns that would improve your code safety and refactor this month.


Checkpoint: Do You Know...?

  • How generics create zero-cost abstractions?
  • When to use GATs for lifetime safety?
  • How sealed traits prevent API misuse?
  • How to encode validation in types?
  • How type states prevent invalid operations?
  • How to manage resource lifecycles?
  • How builders simplify test setup?
  • Why timeouts matter?
  • How to implement type-safe features?
  • When to use macros for enforcement?

If yes to most, you've mastered Design Patterns! 🎉


Congratulations! You now understand the complete pattern language of Chicago TDD Tools.

Final Advice

These patterns work together:

  • Testing patterns help you verify code works
  • Architecture patterns help you organize code
  • Design patterns help you prevent bugs at compile-time

Master them all, and you'll write code that's: ✅ Tested thoroughly ✅ Well-organized ✅ Type-safe ✅ High-performance ✅ Hard to misuse

That's the Chicago TDD difference.

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

🔧 HOW-TO | Structure every test with Arrange-Act-Assert clarity

Quick Reference

AspectDetails
Problem SolvedTests that intermingle setup, behavior, and assertions become hard to read and debug
Core SolutionDivide test into three explicit phases: Arrange, Act, Assert
When to Use✅ All unit tests, ✅ Integration tests, ✅ Even simple assertions
When NOT to Use❌ Property-based tests (different structure), ❌ Complex multi-stage workflows (use fixtures)
DifficultyLow - Easy to learn and apply immediately
Trade-offsSlight verbosity for clarity
Related PatternsError Path Testing, Real Collaborators, Fixture Lifecycle

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

Basic Example: Simple Calculation

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

test!(test_scaling_multiplier, {
    // Arrange: Set up test data
    let multiplier = 3;
    let input = 7;

    // Act: Execute the behavior
    let result = multiplier * input;

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

Async Example: Fetching Data

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

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

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

    // Assert: Verify outcome
    assert_eq!(customer.id, "cust-123");
    Ok(())
});
}

With Fixtures: Complex Setup

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

fixture_test!(test_user_creation, fixture, {
    // Arrange: Create fixture and prepare state
    fixture.set_metadata("user_type", "admin");
    let db = fixture.database();

    // Act: Execute business logic
    let user = db.create_user("alice", "admin")?;

    // Assert: Verify the result
    assert_eq!(user.name, "alice");
    assert_eq!(user.role, "admin");
});
}

Implementation Checklist

  • Arrange phase clearly sets up all necessary test data
  • Act phase calls the function or method exactly once
  • Assert phase verifies the result with specific assertions
  • Comments label each phase (even if obvious)
  • Test name describes what is being tested
  • Each test tests one behavior
  • No logic in assertions (e.g., no if statements)

Common Mistakes & How to Avoid Them

❌ Mistake 1: Mixing Arrange and Act

#![allow(unused)]
fn main() {
// DON'T: Hidden state between phases
test!(test_bad_mixing, {
    let result = setup_and_call();  // Mixes arrange + act
    assert_eq!(result, 42);
});
}
#![allow(unused)]
fn main() {
// DO: Clear separation
test!(test_good_mixing, {
    // Arrange
    let input = 42;

    // Act
    let result = calculate(input);

    // Assert
    assert_eq!(result, 42);
});
}

❌ Mistake 2: Multiple Assertions Without Context

#![allow(unused)]
fn main() {
// DON'T: No comment explaining what each asserts
test!(test_bad_assertions, {
    // ... arrange and act ...
    assert_eq!(user.id, 1);
    assert_eq!(user.name, "Alice");
    assert!(user.is_active);
});
}
#![allow(unused)]
fn main() {
// DO: Clear what each assertion verifies
test!(test_good_assertions, {
    // ... arrange and act ...

    // Assert: User created with correct ID
    assert_eq!(user.id, 1);

    // Assert: User name matches input
    assert_eq!(user.name, "Alice");

    // Assert: User is active by default
    assert!(user.is_active);
});
}

❌ Mistake 3: Act Phase Doing Multiple Things

#![allow(unused)]
fn main() {
// DON'T: Multiple behaviors in Act
test!(test_bad_act, {
    let user = db.create_user("alice");
    user.send_welcome_email();  // Second behavior!
    assert!(user.email_sent);
});
}
#![allow(unused)]
fn main() {
// DO: One behavior per test
test!(test_user_creation, {
    let user = db.create_user("alice");
    assert_eq!(user.name, "alice");
});

test!(test_welcome_email, {
    let user = db.create_user("alice");
    user.send_welcome_email();
    assert!(user.email_sent);
});
}

Real-World Example

The AAA pattern is foundational to Chicago TDD Tools. Every example test follows this structure, found throughout:

  • examples/basic_test.rs - Simple unit tests
  • tests/common.rs - Shared test utilities
  • Test suites in the source code

Advanced: Multiple Assertions in Assert Phase

Allowed: Multiple assertions that verify the same behavior:

#![allow(unused)]
fn main() {
test!(test_user_properties, {
    // Arrange
    let user_data = vec!["alice", "25", "alice@example.com"];

    // Act
    let user = parse_user_csv(&user_data)?;

    // Assert: All properties of the user
    assert_eq!(user.name, "alice");
    assert_eq!(user.age, 25);
    assert_eq!(user.email, "alice@example.com");
    assert!(user.is_valid());
});
}

This is acceptable because all assertions verify related properties of the same object.

  • Pattern 2: Error Path Testing - Same AAA structure for error cases
  • Pattern 3: Boundary Conditions - AAA structure for edge cases
  • Pattern 5: Real Collaborators - AAA with actual dependencies
  • Pattern 16: Fixture Lifecycle Management - AAA with fixtures for complex setup
  • Pattern 17: Builder-Driven Test Data - AAA with fluent builders for Arrange phase

Next Steps

Learn these related patterns to master test structure:

  1. Next: Pattern 2: Error Path Testing - How to test failure cases
  2. Then: Pattern 5: Real Collaborators - Testing with actual dependencies
  3. Advanced: Pattern 16: Fixture Lifecycle - Complex multi-phase tests

Summary

The AAA pattern is the foundation of readable, maintainable tests. By consistently separating Arrange, Act, and Assert, you create tests that:

✅ Are easy to read and understand ✅ Fail with clear messages about what went wrong ✅ Scale from simple to complex tests ✅ Can be easily maintained by teammates

Pro tip: Use comments to label each phase, even in simple tests. It takes one line and makes the intent crystal clear.

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