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.
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)
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!
}- ⏱️ 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
Work in Progress - Early development
Current version: 0.1.0-alpha
| 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 | ❌ 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 |
| Feature | testkit-async | tokio-test | futures-test | async-test |
|---|---|---|---|---|
| Mock Clock | ✅ Full control | ❌ | ❌ | |
| 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
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());
}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
}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());
}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;
}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();
}// Before: Test suite takes 5 minutes (lots of sleeps/timeouts)
// After: Test suite takes 5 seconds (virtual time)// Before: Flaky tests due to race conditions
// After: Deterministic execution, reproducible failures// Test resilience to:
// - Network timeouts
// - Random failures
// - Slow responses
// - Connection drops// Test complex async interactions:
// - Multiple services communicating
// - Event-driven systems
// - Stream processing pipelines# Not yet published - coming soon!
cargo add --dev testkit-async
# Or in Cargo.toml:
[dev-dependencies]
testkit-async = "0.1"- Mock clock implementation
- Time advancement APIs
- Integration with tokio::time
- Pause/resume time
- Test executor
- Sync points
- Step-by-step execution
- Task inspection
- Failure injector
- Network simulator
- Latency injection
- Resource exhaustion simulation
- Async assertion macros
- Stream testing helpers
- Mock trait generation
- Snapshot testing for async
- Tokio integration
- async-std integration
- smol integration
- Runtime-agnostic core
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
Contributions welcome! This project is in early stages.
Priority areas:
- Mock clock implementation
- Test executor design
- Failure injection patterns
- Documentation and examples
- Runtime compatibility
MIT OR Apache-2.0
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
testkit-async - Making async testing practical 🧰
Status: 🚧 Pre-alpha - Core architecture in design
Star ⭐ this repo to follow development!