Rust Async Testing Patterns
This skill provides best practices for testing async Rust code with Tokio in the episodic memory system.
Core Patterns
1. Basic Async Test Setup
use tokio::test;
#[tokio::test]
async fn test_episode_creation() {
let memory = SelfLearningMemory::new(Default::default()).await?;
let episode_id = memory.start_episode(
"Test task".to_string(),
TaskContext::default(),
TaskType::CodeGeneration,
).await;
assert!(!episode_id.is_empty());
}
2. Time-Based Testing with Paused Time
For operations involving tokio::time::sleep or timeouts:
#[tokio::test(start_paused = true)]
async fn test_pattern_extraction_timeout() {
let memory = setup_memory().await;
// Time advances only when awaited
let start = tokio::time::Instant::now();
tokio::time::sleep(Duration::from_secs(5)).await;
// Actual elapsed time is near-zero
assert!(start.elapsed().as_millis() < 100);
}
3. Concurrent Operation Testing
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_episode_operations() {
let memory = Arc::new(setup_memory().await);
let handles: Vec<_> = (0..10)
.map(|i| {
let memory = memory.clone();
tokio::spawn(async move {
memory.start_episode(
format!("Task {}", i),
TaskContext::default(),
TaskType::CodeGeneration,
).await
})
})
.collect();
let results = futures::future::join_all(handles).await;
assert_eq!(results.len(), 10);
}
4. Storage Backend Mocking
#[tokio::test]
async fn test_episode_with_mock_storage() {
let mock_turso = MockTursoStorage::new();
let mock_redb = MockRedbCache::new();
mock_turso.expect_create_episode()
.times(1)
.returning(|_| Ok(()));
let memory = SelfLearningMemory::with_storage(
mock_turso,
mock_redb,
).await?;
memory.start_episode(/* ... */).await;
}
5. Timeout Testing
#[tokio::test]
async fn test_operation_timeout() {
let result = tokio::time::timeout(
Duration::from_secs(2),
slow_operation()
).await;
assert!(result.is_err(), "Operation should timeout");
}
Memory-Specific Patterns
Episode Lifecycle Testing
#[tokio::test]
async fn test_complete_episode_lifecycle() {
let memory = setup_memory().await;
// 1. Start episode
let episode_id = memory.start_episode(/* ... */).await;
// 2. Log execution steps
memory.log_execution_step(/* ... */).await;
// 3. Complete episode
memory.complete_episode(
episode_id,
TaskOutcome::Success,
None,
).await?;
// 4. Verify storage
let episode = memory.get_episode(&episode_id).await?;
assert_eq!(episode.outcome, TaskOutcome::Success);
}
Pattern Extraction Testing
#[tokio::test]
async fn test_async_pattern_extraction() {
let memory = setup_memory().await;
let episode_id = create_test_episode(&memory).await;
// Pattern extraction runs asynchronously
memory.extract_patterns(episode_id).await?;
// Wait for async workers to complete
tokio::time::sleep(Duration::from_millis(100)).await;
let patterns = memory.get_patterns_for_episode(&episode_id).await?;
assert!(!patterns.is_empty());
}
Best Practices
- Use
#[tokio::test]instead oftokio::test::block_on- cleaner syntax - Enable
start_paused = truefor time-dependent tests - faster execution - Use
flavor = "multi_thread"for concurrency tests - realistic scenarios - Always clean up test data - prevent test pollution
- Mock external dependencies - faster, more reliable tests
- Test error paths - async error handling is critical
Common Pitfalls
- Don't use blocking operations in async tests
// BAD
std::thread::sleep(Duration::from_secs(1));
// GOOD
tokio::time::sleep(Duration::from_secs(1)).await;
- Don't forget to await futures
// BAD - creates future but doesn't run it
memory.start_episode(/* ... */);
// GOOD
memory.start_episode(/* ... */).await;
- Don't use single-threaded runtime for concurrency tests
// BAD - uses single-threaded runtime
#[tokio::test]
async fn test_concurrent_ops() { /* ... */ }
// GOOD
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_ops() { /* ... */ }
Resources
- View
resources/tokio-patterns.mdfor advanced patterns - View
resources/concurrent-ops.mdfor parallel execution strategies - View
resources/mock-storage.mdfor storage mocking patterns