Testing async code com tokio-test
1. Introdução ao tokio-test
Testar código assíncrono em Rust apresenta desafios únicos. Diferente de funções síncronas, futures precisam de um runtime para serem executadas, e o comportamento temporal pode tornar testes lentos ou imprevisíveis. O tokio-test é uma biblioteca auxiliar que fornece ferramentas para testar código assíncrono baseado no runtime Tokio de forma determinística e eficiente.
Enquanto testes síncronos executam imediatamente e sequencialmente, testes assíncronos exigem que o runtime agende e execute as tarefas. O tokio-test resolve isso oferecendo utilitários como block_on, assert_ready e controle manual do relógio virtual.
Para instalar, adicione ao Cargo.toml:
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
tokio-test = "0.4"
2. Fundamentos do block_on e assert_ready
O block_on permite executar um future em um contexto síncrono, bloqueando a thread atual até a conclusão. É útil para testes unitários simples.
use tokio_test::block_on;
async fn add(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn test_add_async() {
let result = block_on(add(2, 3));
assert_eq!(result, 5);
}
Alternativamente, o macro #[tokio::test] gerencia o runtime automaticamente:
#[tokio::test]
async fn test_add_with_macro() {
let result = add(2, 3).await;
assert_eq!(result, 5);
}
Para testar estados parciais de futures, use assert_ready e assert_pending:
use tokio_test::{assert_ready, assert_pending};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
#[tokio::test]
async fn test_future_states() {
let mut future = Box::pin(add(1, 2));
let waker = tokio_test::task::noop_waker();
let mut cx = Context::from_waker(&waker);
assert_ready!(future.as_mut().poll(&mut cx));
}
3. Testando com tokio::test e gerenciamento de runtime
O macro #[tokio::test] aceita configurações de runtime:
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_concurrent_tasks() {
let handle = tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
42
});
let result = handle.await.unwrap();
assert_eq!(result, 42);
}
Para testar panics em código assíncrono:
async fn might_panic(should_panic: bool) -> i32 {
if should_panic {
panic!("deu ruim!");
}
0
}
#[tokio::test]
#[should_panic(expected = "deu ruim!")]
async fn test_async_panic() {
might_panic(true).await;
}
4. Simulação de tempo com tokio_test::clock
Testar timeouts sem esperar o tempo real é essencial. O tokio_test::clock permite avançar o relógio manualmente:
use tokio_test::clock;
use tokio::time::{sleep, Duration};
#[tokio::test]
async fn test_sleep_with_clock() {
clock::pause(); // Congela o relógio
let sleep_future = sleep(Duration::from_secs(5));
tokio::pin!(sleep_future);
// Avança 3 segundos
clock::advance(Duration::from_secs(3)).await;
assert!(sleep_future.as_mut().poll(&mut tokio_test::task::spawn(()).cx()).is_pending());
// Avança mais 2 segundos para completar
clock::advance(Duration::from_secs(2)).await;
assert!(sleep_future.as_mut().poll(&mut tokio_test::task::spawn(()).cx()).is_ready());
}
Testando um timeout real:
use tokio::time::{timeout, Duration};
async fn slow_operation() -> &'static str {
sleep(Duration::from_secs(10)).await;
"concluído"
}
#[tokio::test]
async fn test_timeout() {
clock::pause();
let result = timeout(Duration::from_secs(5), slow_operation());
tokio::pin!(result);
clock::advance(Duration::from_secs(3)).await;
assert!(result.as_mut().poll(&mut tokio_test::task::spawn(()).cx()).is_pending());
clock::advance(Duration::from_secs(3)).await;
let output = assert_ready!(result.as_mut().poll(&mut tokio_test::task::spawn(()).cx()));
assert!(output.is_err()); // Timeout ocorreu
}
5. Testando streams e canais com tokio_test
Para testar streams assíncronos, use block_on_stream:
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use tokio_test::block_on_stream;
#[tokio::test]
async fn test_mpsc_channel() {
let (tx, rx) = mpsc::channel(10);
tokio::spawn(async move {
for i in 0..3 {
tx.send(i).await.unwrap();
}
});
let mut stream = block_on_stream(rx);
assert_eq!(stream.next(), Some(1));
assert_eq!(stream.next(), Some(2));
assert_eq!(stream.next(), Some(3));
assert_eq!(stream.next(), None);
}
Testando estados de canais com assert_ready:
use tokio::sync::oneshot;
#[tokio::test]
async fn test_oneshot() {
let (tx, mut rx) = oneshot::channel();
tokio::spawn(async move {
tx.send(42).unwrap();
});
let mut task = tokio_test::task::spawn(async {
rx.await.unwrap()
});
assert_pending!(task.poll());
// Após o spawn completar:
assert_eq!(task.await, 42);
}
6. Mock de recursos externos com tokio::io e tokio::net
O tokio_test::io::Builder simula operações de I/O:
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_test::io::Builder;
#[tokio::test]
async fn test_mock_io() {
let mut mock = Builder::new()
.read(b"hello world")
.write(b"echo: hello world")
.build();
let mut buf = vec![0u8; 11];
mock.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"hello world");
mock.write_all(b"echo: hello world").await.unwrap();
}
Testando um servidor TCP simulado:
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn test_tcp_server() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 1024];
let n = socket.read(&mut buf).await.unwrap();
socket.write_all(&buf[..n]).await.unwrap();
});
let mut client = tokio::net::TcpStream::connect(addr).await.unwrap();
client.write_all(b"ping").await.unwrap();
let mut buf = vec![0u8; 4];
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
server.await.unwrap();
}
7. Padrões avançados e boas práticas
Combinando tokio-test com mockall para mocks de dependências:
use mockall::automock;
#[automock]
trait AsyncService {
async fn fetch_data(&self) -> Result<String, String>;
}
#[tokio::test]
async fn test_with_mock() {
let mut mock = MockAsyncService::new();
mock.expect_fetch_data()
.returning(|| Box::pin(async { Ok("mocked data".to_string()) }));
let result = mock.fetch_data().await;
assert_eq!(result.unwrap(), "mocked data");
}
Testando graceful shutdown:
use tokio::sync::oneshot;
async fn server_loop(mut shutdown_rx: oneshot::Receiver<()>) {
tokio::select! {
_ = &mut shutdown_rx => {
println!("Shutting down gracefully");
}
}
}
#[tokio::test]
async fn test_graceful_shutdown() {
let (tx, rx) = oneshot::channel();
let handle = tokio::spawn(server_loop(rx));
tx.send(()).unwrap();
tokio::time::timeout(Duration::from_millis(100), handle)
.await
.expect("Server should shut down quickly")
.unwrap();
}
Dicas para evitar race conditions:
- Use tokio_test::clock::pause() para controlar o tempo
- Evite tokio::spawn desnecessário em testes unitários
- Prefira assert_ready! e assert_pending! para verificar estados intermediários
- Mantenha testes determinísticos usando tokio_test::task::spawn
8. Limitações e alternativas
O tokio-test é excelente para testes unitários e de integração controlados, mas tem limitações:
- Testes de integração reais: Para testar sistemas distribuídos ou I/O real, use
#[tokio::test]sem manipulação de clock - Mocks complexos: Para dependências externas, combine com
mockallou crie stubs manuais - Performance: Testes com
block_onem múltiplas threads podem ser lentos; prefira#[tokio::test]single-thread
Alternativas:
- futures-test: Similar, mas para o ecossistema futures-rs
- async-std: Runtime alternativo com seus próprios utilitários de teste
- Testes manuais com block_on simples para casos triviais
Para suites de teste grandes, considere separar testes unitários (com tokio-test) de testes de integração (com runtime real) para garantir velocidade e precisão.
Referências
- Documentação oficial do tokio-test — Referência completa da API com exemplos de uso
- Tokio Testing Guide — Guia oficial da equipe Tokio sobre boas práticas de teste
- Testing async code in Rust - Tokio Blog — Artigo detalhado sobre desafios e soluções para testes assíncronos
- Mockall crate documentation — Como criar mocks para funções e traits assíncronas
- Async Testing Patterns in Rust - Rust Magazine — Padrões avançados de teste para código assíncrono
- futures-test crate documentation — Alternativa para testar futures do ecossistema futures-rs
- Testing Tokio Channels - Tokio Tutorial — Exemplos práticos de teste de canais com tokio-test