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:
- Testing Patterns – maintainable, behavior-driven tests that fail fast and verify real outcomes.
- Architecture Patterns – structural choices that keep the framework extensible and consistent.
- 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 Situation | Go To | Time |
|---|---|---|
| I need to solve a problem | Choosing Your Pattern | 5 min |
| I want a quick overview | All Patterns Reference | 10 min |
| I want to learn systematically | Choose a learning sequence below | 60-120 min |
| I want a specific pattern | Jump to pattern reference below | Varies |
Learning Sequences
Learn patterns progressively through structured tutorials:
| Sequence | Focus | Time | Difficulty |
|---|---|---|---|
| Testing Patterns | How to write better tests | 90 min | Beginner |
| Architecture Patterns | How to organize code | 60 min | Intermediate |
| Design Patterns | Type safety and optimization | 120 min | Advanced |
Recommended: Follow in order (Testing → Architecture → Design).
Quick Links
- Choosing Your Pattern - Decision matrices to find the right pattern
- All Patterns Reference - All 20 patterns at a glance
- Testing Patterns - Learn to write great tests
- Architecture Patterns - Learn to organize code
- Design Patterns - Learn advanced safety and 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 Challenge | Pattern Family | Pattern Name | Go To |
|---|---|---|---|
| How do I structure a test? | Testing | Pattern 1: AAA Pattern | Learn |
| How do I test error cases? | Testing | Pattern 2: Error Path Testing | Learn |
| How do I test edge cases? | Testing | Pattern 3: Boundary Conditions | Learn |
| How do I clean up resources? | Testing | Pattern 4: Resource Cleanup | Learn |
| Should I use mocks? | Testing | Pattern 5: Real Collaborators | Learn |
| How do I organize my code? | Architecture | Pattern 6: Generic Base Layer | Learn |
| How do I extend without duplicating? | Architecture | Pattern 7: Extension Layer | Learn |
| How do I avoid code duplication? | Architecture | Pattern 8: Composition Over Duplication | Learn |
| How do I avoid data inconsistency? | Architecture | Pattern 9: Single Source of Truth | Learn |
| How do I organize large modules? | Architecture | Pattern 10: Capability Grouping | Learn |
| How do I optimize for performance? | Design | Pattern 11: Zero-Cost Abstractions | Learn |
| How do I prevent type errors? | Design | Pattern 12: Type Safety with GATs | Learn |
| How do I prevent API misuse? | Design | Pattern 13: Sealed Traits | Learn |
| How do I validate at compile-time? | Design | Pattern 14: Compile-Time Validation | Learn |
| How do I enforce state machines? | Design | Pattern 15: Type State Enforcement | Learn |
| How do I manage fixture lifecycle? | Design | Pattern 16: Fixture Lifecycle Management | Learn |
| How do I build test data easily? | Design | Pattern 17: Builder-Driven Test Data | Learn |
| How do I prevent timeouts? | Design | Pattern 18: Timeout Defense | Learn |
| How do I manage feature flags? | Design | Pattern 19: Feature Gate Slices | Learn |
| How do I enforce patterns with macros? | Design | Pattern 20: Macro Pattern Enforcement | Learn |
By Category
Testing Patterns: "How Do I Write Better Tests?"
These patterns solve fundamental testing problems:
| Pattern | Problem | Solution |
|---|---|---|
| Pattern 1: AAA | Tests are hard to read | Structure: Arrange, Act, Assert |
| Pattern 2: Error Paths | I don't test failures | Test both success and error cases |
| Pattern 3: Boundaries | I miss edge cases | Systematically test limits |
| Pattern 4: Resource Cleanup | Tests leak resources | Automatic fixture cleanup |
| Pattern 5: Real Collaborators | Mocks hide integration bugs | Test 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:
| Pattern | Problem | Solution |
|---|---|---|
| Pattern 6: Generic Base | Code is duplicate | Extract generic abstractions |
| Pattern 7: Extension Layer | I can't extend without modifying | Add layers for extensions |
| Pattern 8: Composition | DRY violations everywhere | Compose instead of duplicating |
| Pattern 9: Single Source | Data gets out of sync | One canonical source of truth |
| Pattern 10: Capability Groups | Module is too large | Organize 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:
| Pattern | Problem | Solution |
|---|---|---|
| Pattern 11: Zero-Cost | Abstractions are slow | Zero-cost abstractions via generics |
| Pattern 12: Type Safety | Type errors at runtime | Use GATs for safety |
| Pattern 13: Sealed Traits | API is too easy to misuse | Seal traits to prevent misuse |
| Pattern 14: Compile-Time | Errors caught at runtime | Validate at compile-time |
| Pattern 15: Type State | State machines are error-prone | Encode states in types |
| Pattern 16: Fixture Lifecycle | Test setup is complex | Manage lifecycle with traits |
| Pattern 17: Builder Test Data | Building test data is tedious | Fluent builders for test data |
| Pattern 18: Timeout Defense | Tests hang forever | Timeout defense in depth |
| Pattern 19: Feature Gates | Feature flags are unreliable | Gate slices across codebase |
| Pattern 20: Macro Enforcement | Patterns are easy to violate | Use 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)
- Pattern 1: AAA Pattern - Foundation
- Pattern 2: Error Path Testing - See what not to do
- Pattern 3: Boundary Conditions - Edge cases matter
- Pattern 4: Resource Cleanup - Don't leak resources
- Pattern 5: Real Collaborators - Test with real code
Time: ~3 hours | Content: Testing fundamentals
Intermediate (Build on Basics)
- Pattern 6: Generic Base Layer - Code organization
- Pattern 8: Composition Over Duplication - DRY principle
- Pattern 10: Capability Grouping - Module organization
- Pattern 17: Builder-Driven Test Data - Practical testing
- Pattern 14: Compile-Time Validation - Type safety
Time: ~4 hours | Content: Architecture and safety
Advanced (Master the Craft)
- Pattern 11: Zero-Cost Abstractions - Performance
- Pattern 12: Type Safety with GATs - Advanced types
- Pattern 15: Type State Enforcement - State machines
- Pattern 13: Sealed Traits - API design
- 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:
- Pattern 1: AAA - Structure your tests
- Pattern 2: Error Paths - Test failures
- Pattern 3: Boundaries - Test edge cases
- Pattern 4: Resource Cleanup - Clean automatically
- Pattern 5: Real Collaborators - Use real dependencies
- 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:
- Pattern 6: Generic Base - Reusable abstractions
- Pattern 7: Extension Layer - Allow extensibility
- Pattern 13: Sealed Traits - Prevent misuse
- Pattern 14: Compile-Time Validation - Validate early
- Pattern 15: Type State - Enforce state machines
- Pattern 20: Macro Enforcement - Enforce usage patterns
Expected outcome: Safe, extensible, hard-to-misuse API
"I want maximum performance"
Use these patterns together:
- Pattern 11: Zero-Cost Abstractions - Generic dispatch
- Pattern 12: Type Safety with GATs - Type-safe lifetimes
- Pattern 14: Compile-Time Validation - Zero runtime checks
- 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:
- Testing Patterns Learning Path (90 minutes)
- Architecture Patterns Learning Path (60 minutes)
- Design Patterns Learning Path (120 minutes)
- All Patterns Quick Reference (Lookup table for all 20)
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)
| # | Pattern | Problem | Solution | Learn More |
|---|---|---|---|---|
| 1 | AAA Pattern | Tests are unreadable | Structure into Arrange-Act-Assert | → |
| 2 | Error Path Testing | Failures aren't tested | Test both success AND error paths | → |
| 3 | Boundary Conditions | Edge cases are missed | Systematically test limits | → |
| 4 | Resource Cleanup | Tests leak resources | Automatic fixture cleanup | → |
| 5 | Real Collaborators | Mocks hide bugs | Test with real implementations | → |
Key: These 5 patterns are the foundation. Use them in every test.
Architecture Patterns (5)
| # | Pattern | Problem | Solution | Learn More |
|---|---|---|---|---|
| 6 | Generic Base Layer | Code duplication | Extract generic abstractions | → |
| 7 | Extension Layer | Can't extend without modifying | Add layers for safe extension | → |
| 8 | Composition Over Duplication | DRY violations | Compose instead of copying | → |
| 9 | Single Source of Truth | Data inconsistencies | One canonical source | → |
| 10 | Capability Grouping | Monolithic modules | Organize by capability | → |
Key: These 5 patterns organize code structure. Use during architecture phase.
Design Patterns (10)
| # | Pattern | Problem | Solution | Learn More |
|---|---|---|---|---|
| 11 | Zero-Cost Abstractions | Abstractions are slow | Use generics, compile away overhead | → |
| 12 | Type Safety with GATs | Type errors at runtime | Generic Associated Types | → |
| 13 | Sealed Traits | API is too easy to misuse | Seal traits to prevent misuse | → |
| 14 | Compile-Time Validation | Errors caught at runtime | Validate during compilation | → |
| 15 | Type State Enforcement | State machines are error-prone | Encode states in the type system | → |
| 16 | Fixture Lifecycle | Complex test setup | Manage with sealed traits | → |
| 17 | Builder-Driven Test Data | Building test data is tedious | Fluent builders for data | → |
| 18 | Timeout Defense | Tests hang indefinitely | Timeout defense in depth | → |
| 19 | Feature Gate Slices | Feature flags are unreliable | Slice-based feature gating | → |
| 20 | Macro Pattern Enforcement | Patterns are easy to violate | Use 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)
- Pattern 1: AAA Pattern
- Pattern 2: Error Path Testing
- 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
- Find your problem in the Problem column
- See the solution in the Solution column
- Click Learn More to read the full pattern
- Bookmark the pattern for future reference
Quick Links
| Want to... | Go to... |
|---|---|
| Choose a pattern | Choosing Your Pattern |
| Learn testing | Testing Learning Sequence |
| Learn architecture | Architecture Learning Sequence |
| Learn design | Design Learning Sequence |
| All 20 patterns | This 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
| Metric | Value |
|---|---|
| Total Patterns | 20 |
| Testing Patterns | 5 |
| Architecture Patterns | 5 |
| Design Patterns | 10 |
| Difficulty Range | Beginner → Advanced |
| Total Learning Time | ~10 hours |
| Estimated Implementation | 2-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
| Pattern | Time | Focus |
|---|---|---|
| Pattern 1: AAA | 15 min | Test structure and readability |
| Pattern 2: Error Paths | 20 min | Testing failures and error cases |
| Pattern 3: Boundaries | 15 min | Edge cases and limits |
| Pattern 4: Resource Cleanup | 15 min | Automatic cleanup and fixtures |
| Pattern 5: Real Collaborators | 15 min | Testing with real dependencies |
| Practice Exercises | 15 min | Apply 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:
- Arrange: Set up the test data and environment
- Act: Execute the behavior you're testing
- 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:
- Creates a user
- 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:
- Normal case - Everything works perfectly
- Invalid input - Bad data
- Boundary case - Limits (empty, max size, etc.)
- Error case - Something fails
- 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:
- ✅ Normal:
divide(10, 2)→Ok(5) - ✅ Error:
divide(10, 0)→Err(Division by zero) - ✅ 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
| Boundary | Test Values | Example |
|---|---|---|
| Empty | 0 items | vec![] |
| Single | 1 item | vec![1] |
| Multiple | 2-many items | vec![1, 2, 3...] |
| Negative | Negative numbers | -1, -100 |
| Maximum | Max value | u32::MAX |
| Minimum | Min value | u32::MIN |
Checkpoint Question
You're testing a split_string(s, limit) function.
What boundary cases should you test?
Answers:
- ✅ Empty string
- ✅ Limit = 0
- ✅ Limit = 1
- ✅ Limit > length
- ✅ 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:
- Pattern 1 (AAA): Structure tests clearly
- Pattern 2 (Error Paths):
- Empty email ❌
- Short password ❌
- User already exists ❌
- Valid registration ✅
- Pattern 3 (Boundaries):
- Minimum password length (7, 8, 9 chars)
- Very long email
- Pattern 4 (Resource Cleanup):
- Use fixture for database cleanup
- 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
| Pattern | Goal | Use When |
|---|---|---|
| Pattern 1: AAA | Readable tests | Every test |
| Pattern 2: Error Paths | Test failures | Every function |
| Pattern 3: Boundaries | Test limits | Every input |
| Pattern 4: Resource Cleanup | Auto cleanup | Complex tests |
| Pattern 5: Real Collaborators | Real deps | Integration 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
- Full Pattern Details: Testing Patterns
- Decision Guide: Choosing Your Pattern
- All Patterns: Quick Reference
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
| Pattern | Time | Focus |
|---|---|---|
| Pattern 6: Generic Base Layer | 15 min | Eliminating duplication with abstractions |
| Pattern 7: Extension Layer | 12 min | Safe extensibility without modification |
| Pattern 8: Composition Over Duplication | 12 min | DRY principle in action |
| Pattern 9: Single Source of Truth | 12 min | Keeping data consistent |
| Pattern 10: Capability Grouping | 9 min | Organizing 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
| Aspect | Composition | Inheritance |
|---|---|---|
| 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
| Domain | Truth | Derived |
|---|---|---|
| E-commerce | Database (orders) | Cache, indices, reports |
| Auth | Database (permissions) | Session tokens, caches |
| Analytics | Raw events | Aggregations, 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:
- Accept orders
- Store them in database
- Can switch between PostgreSQL and SQLite
- Add logging without modifying core code
- Keep inventory count accurate
- 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
| Pattern | Problem | Solution |
|---|---|---|
| 6: Generic Base | Duplication | Traits and abstraction |
| 7: Extension Layer | Modification | Wrapping instead of changing |
| 8: Composition | Copy-paste | Share components |
| 9: Single Source | Inconsistency | One source of truth |
| 10: Capability | Disorganization | Group 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
| Pattern | Time | Focus |
|---|---|---|
| Pattern 11: Zero-Cost Abstractions | 12 min | Performance without sacrifice |
| Pattern 12: Type Safety with GATs | 12 min | Lifetimes and type safety |
| Pattern 13: Sealed Traits | 10 min | Prevent API misuse |
| Pattern 14: Compile-Time Validation | 12 min | Move errors left |
| Pattern 15: Type State Enforcement | 12 min | Encode state in types |
| Pattern 16: Fixture Lifecycle | 12 min | Safe resource management |
| Pattern 17: Builder-Driven Test Data | 12 min | Fluent test builders |
| Pattern 18: Timeout Defense | 12 min | Prevent hangs |
| Pattern 19: Feature Gate Slices | 12 min | Reliable feature flags |
| Pattern 20: Macro Pattern Enforcement | 12 min | Enforce 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
| Choice | Cost | Use When |
|---|---|---|
| Generics | Compile-time | Size known, type fixed |
| Trait objects | Runtime | Size 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
| Pattern | Types | Purpose |
|---|---|---|
| Builder pattern | Raw → Built | Ensure all fields set |
| State machines | Idle → Running | Prevent invalid operations |
| Type tokens | Foo<_> with phantom | Track type information |
Checkpoint Question
You have an API that requires:
- Initialize
- Configure
- 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:
- Begin
- Execute
- 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:
| # | Pattern | What It Does |
|---|---|---|
| 16 | Fixture Lifecycle | Manage resources with sealed traits (combines patterns 13+2) |
| 17 | Builder Test Data | Fluent builders for test setup (applies compilation validation) |
| 18 | Timeout Defense | Multiple timeout strategies (zero-cost + compile-time) |
| 19 | Feature Gates | Type-safe feature flags (type state + sealed traits) |
| 20 | Macro Enforcement | Compile-time pattern checks via macros (ultimate compile-time validation) |
Putting It Together: Complete Design
Exercise: Build a Safe Transaction System
Requirements:
- Transactions must go: Begin → Execute → Commit
- Can't add statements after commit
- Type-safe with zero runtime overhead
- Can't be misused by users
- 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
| Pattern | Goal | Mechanism |
|---|---|---|
| 11 | Zero-cost | Generics + monomorphization |
| 12 | Lifetimes | GATs (Generic Associated Types) |
| 13 | API safety | Sealed traits |
| 14 | Validation | Phantom types + state |
| 15 | State machines | Type states |
| 16 | Resources | Lifecycle traits |
| 17 | Test data | Fluent builders |
| 18 | Robustness | Timeout strategies |
| 19 | Features | Type-safe gates |
| 20 | Enforcement | Procedural macros |
Next Steps
Master One Pattern Per Week
- Week 1: Zero-Cost & GATs
- Week 2: Sealed & Compile-Time
- Week 3: Type State & Lifecycle
- 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
| Aspect | Details |
|---|---|
| Problem Solved | Tests that intermingle setup, behavior, and assertions become hard to read and debug |
| Core Solution | Divide 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) |
| Difficulty | Low - Easy to learn and apply immediately |
| Trade-offs | Slight verbosity for clarity |
| Related Patterns | Error 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
ifstatements)
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 teststests/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.
Related Patterns
- 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:
- Next: Pattern 2: Error Path Testing - How to test failure cases
- Then: Pattern 5: Real Collaborators - Testing with actual dependencies
- 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)); } } }
Related Patterns
- 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"), } } } }
Related Patterns
- 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.
Related Patterns
- 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>(()) }); }
Related Patterns
- 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}; }
Related Patterns
- 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 } } } }
Related Patterns
- 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") } } }
Related Patterns
- 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; }
Related Patterns
- 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 }
Related Patterns
- 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) } }
Related Patterns
- 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>>; } }
Related Patterns
- 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 { // ... } }
Related Patterns
- 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]>, } }
Related Patterns
- 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> { /* ... */ } } }
Related Patterns
- 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(()) }); }
Related Patterns
- 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") } } }
Related Patterns
- 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:
- Test-level (
tokio::time::timeoutinside macros) – fails the specific test with a clear message. - Runner-level (
cargo-nextestprofiles) – applies SLA-based timeouts per profile. - Process-level (
timeoutwrapper inMakefile.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(()) }); }
Related Patterns
- 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; }
Related Patterns
- 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"); }; } }
Related Patterns
- Pattern 1: AAA Pattern
- Pattern 18: Timeout Defense in Depth
- Pattern 19: Feature Gate Slices