1 unstable release
| 0.0.1 | Nov 18, 2025 |
|---|
#26 in #tokio-test
17KB
testkit-async 🧰
Practical testing tools for async Rust
testkit-async is a comprehensive testing toolkit for async Rust code. It provides time control, deterministic execution, failure injection, and rich assertions to make async testing fast, reliable, and easy.
🎯 Why testkit-async?
The Problem
Testing async code in Rust is frustrating:
#[tokio::test]
async fn test_retry_with_timeout() {
// This test takes 30+ seconds to run! 😱
let result = retry_with_timeout(
failing_operation,
Duration::from_secs(30)
).await;
// How do I know retry happened 3 times?
// How do I test timeout without waiting 30s?
// How do I make this deterministic?
}
Common issues:
- ❌ Tests are slow (waiting for real time)
- ❌ Tests are flaky (race conditions, timing issues)
- ❌ Tests are hard to write (complex async coordination)
- ❌ Tests are unpredictable (non-deterministic execution)
The Solution
use testkit_async::prelude::*;
#[testkit_async::test]
async fn test_retry_with_timeout() {
let clock = MockClock::new();
let counter = AtomicU32::new(0);
// Test runs instantly! ⚡
let future = retry_with_timeout(
|| async {
counter.fetch_add(1, Ordering::SeqCst);
Err("fail")
},
Duration::from_secs(30)
);
// Advance virtual time - no real waiting!
clock.advance(Duration::from_secs(31));
// Verify behavior
assert!(future.await.is_err());
assert_eq!(counter.load(Ordering::SeqCst), 3); // Retried 3 times!
}
✨ Features (Planned)
- ⏱️ Mock Clock - Control time without waiting
- 🎮 Deterministic Executor - Control task execution order
- 💥 Failure Injection - Simulate errors, timeouts, network issues
- 🔍 Async Assertions - Fluent API for testing streams and futures
- 🎯 Sync Points - Coordinate multiple tasks precisely
- 📊 Test Utilities - Mocks, spies, and test helpers
🚧 Status
Work in Progress - Early development
Current version: 0.1.0-alpha
🆚 Comparison with Existing Tools
What Already Exists
| Tool | What It Does | What's Missing |
|---|---|---|
| async-test | Attribute macro for async tests | ❌ No time control ❌ No execution control ❌ Just a macro wrapper |
| tokio-test | Tokio testing utilities | ⚠️ Tokio-specific only ❌ Limited time control ❌ No failure injection |
| futures-test | Futures test utilities | ❌ No mock clock ❌ Low-level only ❌ Not ergonomic |
| mockall | General mocking | ❌ Not async-aware ❌ Verbose for async |
What testkit-async Provides
| Feature | testkit-async | tokio-test | futures-test | async-test |
|---|---|---|---|---|
| Mock Clock | ✅ Full control | ⚠️ Limited | ❌ | ❌ |
| Deterministic Execution | ✅ | ❌ | ❌ | ❌ |
| Failure Injection | ✅ | ❌ | ❌ | ❌ |
| Async Assertions | ✅ | ❌ | ❌ | ❌ |
| Sync Points | ✅ | ❌ | ❌ | ❌ |
| Runtime Agnostic | ✅ | ❌ Tokio only | ✅ | ✅ |
| Ergonomic API | ✅ | ⚠️ | ❌ | ⚠️ |
Key Differentiators:
- Complete Time Control - Not just pause/resume, but full virtual time
- Deterministic Testing - Control exact execution order of tasks
- Chaos Engineering - Built-in failure injection and network simulation
- High-Level API - Ergonomic, not low-level primitives
📚 Quick Examples (Planned API)
Time Control
use testkit_async::prelude::*;
#[testkit_async::test]
async fn test_with_timeout() {
let clock = MockClock::new();
// This completes instantly in tests!
let future = timeout(Duration::from_secs(30), slow_operation());
// Advance virtual time
clock.advance(Duration::from_secs(31));
// Timeout triggered without waiting 30s
assert!(future.await.is_err());
}
Controlled Concurrency
use testkit_async::prelude::*;
#[testkit_async::test]
async fn test_race_condition() {
let executor = TestExecutor::new();
let counter = Arc::new(Mutex::new(0));
// Spawn two tasks
let c1 = counter.clone();
executor.spawn(async move {
sync_point("before").await;
*c1.lock().await += 1;
});
let c2 = counter.clone();
executor.spawn(async move {
sync_point("before").await;
*c2.lock().await += 1;
});
// Release both simultaneously - guaranteed race!
executor.release("before");
executor.run_until_idle().await;
// Now you can test race condition handling
}
Failure Injection
use testkit_async::chaos::FailureInjector;
#[testkit_async::test]
async fn test_retry_logic() {
let injector = FailureInjector::new()
.fail_first(3) // First 3 calls fail
.then_succeed();
let client = HttpClient::new()
.with_interceptor(injector);
let result = retry_request(&client).await?;
// Verify retry worked
assert_eq!(injector.attempt_count(), 4); // 3 failures + 1 success
assert!(result.is_ok());
}
Async Assertions
use testkit_async::prelude::*;
#[testkit_async::test]
async fn test_stream() {
let stream = create_data_stream();
// Fluent assertions for streams
assert_stream!(stream)
.next_eq(1).await
.next_eq(2).await
.next_eq(3).await
.ends().await;
// Timing assertions
assert_completes_within!(
Duration::from_millis(100),
fast_operation()
).await;
}
Mock Async Dependencies
use testkit_async::mock::*;
#[async_trait]
trait DataStore {
async fn fetch(&self, id: u64) -> Result<Data>;
}
#[testkit_async::test]
async fn test_with_mock() {
let mut mock = MockDataStore::new();
// Setup expectations
mock.expect_fetch()
.with(eq(42))
.times(1)
.returning(|_| Ok(Data { value: 100 }));
// Use the mock
let result = process_data(&mock, 42).await?;
// Verify
assert_eq!(result.value, 100);
mock.verify();
}
🎯 Use Cases
Fast Test Suites
// Before: Test suite takes 5 minutes (lots of sleeps/timeouts)
// After: Test suite takes 5 seconds (virtual time)
Deterministic Tests
// Before: Flaky tests due to race conditions
// After: Deterministic execution, reproducible failures
Chaos Engineering
// Test resilience to:
// - Network timeouts
// - Random failures
// - Slow responses
// - Connection drops
Integration Testing
// Test complex async interactions:
// - Multiple services communicating
// - Event-driven systems
// - Stream processing pipelines
📦 Installation
# Not yet published - coming soon!
cargo add --dev testkit-async
# Or in Cargo.toml:
[dev-dependencies]
testkit-async = "0.1"
🗺️ Roadmap
Phase 1: Time Control (Current)
- Mock clock implementation
- Time advancement APIs
- Integration with tokio::time
- Pause/resume time
Phase 2: Execution Control
- Test executor
- Sync points
- Step-by-step execution
- Task inspection
Phase 3: Chaos Engineering
- Failure injector
- Network simulator
- Latency injection
- Resource exhaustion simulation
Phase 4: Assertions & Utilities
- Async assertion macros
- Stream testing helpers
- Mock trait generation
- Snapshot testing for async
Phase 5: Ecosystem Integration
- Tokio integration
- async-std integration
- smol integration
- Runtime-agnostic core
🎨 Design Philosophy
Ergonomics First:
- Simple for common cases
- Powerful for complex scenarios
- Minimal boilerplate
Determinism:
- Reproducible test results
- No timing-dependent failures
- Controlled execution order
Fast:
- Tests run at CPU speed, not wall-clock time
- Parallel-friendly
- Efficient mocking
Composable:
- Mix and match features
- Works with existing tools
- Not all-or-nothing
🤝 Contributing
Contributions welcome! This project is in early stages.
Priority areas:
- Mock clock implementation
- Test executor design
- Failure injection patterns
- Documentation and examples
- Runtime compatibility
📝 License
MIT OR Apache-2.0
🙏 Acknowledgments
Inspired by:
- tokio-test - Tokio testing utilities
- futures-test - Futures testing primitives
- async-std - Async runtime ideas
- Testing frameworks from other languages:
- Python's pytest-asyncio
- JavaScript's fake-timers
- Go's testing patterns
🔗 Related Projects
testkit-async - Making async testing practical 🧰
Status: 🚧 Pre-alpha - Core architecture in design
Star ⭐ this repo to follow development!
Dependencies
~2.8–9MB
~176K SLoC