| Crates.io | testkit-async |
| lib.rs | testkit-async |
| version | 0.0.1 |
| created_at | 2025-11-18 22:35:14.565961+00 |
| updated_at | 2025-11-18 22:35:14.565961+00 |
| description | Practical testing tools for async Rust - time control, deterministic execution, and failure injection |
| homepage | https://github.com/ibrahimcesar/testkit-async |
| repository | https://github.com/ibrahimcesar/testkit-async |
| max_upload_size | |
| id | 1939091 |
| size | 45,460 |
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:
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!
}
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 | ⚠️ 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 |
| 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:
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"
Ergonomics First:
Determinism:
Fast:
Composable:
Contributions welcome! This project is in early stages.
Priority areas:
MIT OR Apache-2.0
Inspired by:
testkit-async - Making async testing practical 🧰
Status: 🚧 Pre-alpha - Core architecture in design
Star ⭐ this repo to follow development!