Mocking dependencies com mockall
1. Introdução ao Mocking em Rust
Em testes de software, isolar a unidade sob teste de suas dependências externas é crucial para garantir previsibilidade e reprodutibilidade. Mocking permite substituir componentes reais (como bancos de dados, APIs HTTP ou sistemas de arquivos) por versões controladas que simulam comportamentos específicos.
O Rust apresenta desafios únicos para mocking devido ao seu sistema de ownership, lifetimes e traits. Diferentemente de linguagens com garbage collection, onde objetos mock podem ser facilmente criados e descartados, no Rust precisamos gerenciar cuidadosamente a propriedade e os tempos de vida dos objetos mock.
A biblioteca mockall surge como uma solução elegante para esses desafios, oferecendo:
- Geração automática de mocks via macros
- Suporte completo a traits com genéricos e lifetimes
- Integração com código assíncrono
- Verificação rigorosa de chamadas e argumentos
2. Configuração Inicial e Primeiro Mock
Para começar, adicione mockall como dependência de desenvolvimento no seu Cargo.toml:
[dev-dependencies]
mockall = "0.12"
Vamos criar um trait simples para mockar — uma interface de banco de dados:
pub trait Database {
fn get_user(&self, id: u32) -> Option<String>;
fn save_user(&mut self, id: u32, name: &str) -> Result<(), String>;
}
Com mockall, podemos gerar automaticamente um mock usando #[automock]:
use mockall::automock;
#[automock]
pub trait Database {
fn get_user(&self, id: u32) -> Option<String>;
fn save_user(&mut self, id: u32, name: &str) -> Result<(), String>;
}
Alternativamente, para maior controle, podemos usar a macro mock!:
mockall::mock! {
pub Database {
fn get_user(&self, id: u32) -> Option<String>;
fn save_user(&mut self, id: u32, name: &str) -> Result<(), String>;
}
}
3. Definindo Expectativas e Comportamentos
O poder do mockall está em suas expectativas. Cada método do mock gera um método expect_* correspondente:
#[test]
fn test_user_service() {
let mut mock_db = MockDatabase::new();
// Espera uma chamada com argumento específico
mock_db.expect_get_user()
.with(predicate::eq(42))
.returning(|_| Some("Alice".to_string()));
// Espera exatamente uma chamada
mock_db.expect_save_user()
.with(predicate::eq(1), predicate::eq("Bob"))
.times(1)
.returning(|_, _| Ok(()));
let service = UserService::new(mock_db);
let user = service.get_user(42);
assert_eq!(user, Some("Alice".to_string()));
}
Matchers permitem especificar argumentos de forma flexível:
use mockall::predicate;
// Qualquer valor u32
.with(predicate::always())
// Função personalizada
.withf(|id: &u32| *id > 0 && *id < 100)
// Combinação de predicados
.with(predicate::eq(42), predicate::in_iter(vec!["Alice", "Bob"]))
4. Mockando Traits com Genéricos e Lifetimes
Traits genéricos requerem configuração adicional. Use #[automock(type ...)] para especificar os tipos concretos:
#[automock]
pub trait Repository<T: Clone + 'static> {
fn find(&self, id: u32) -> Option<T>;
fn save(&mut self, item: T) -> Result<(), String>;
}
// No teste, especifique o tipo concreto:
type MockUserRepo = MockRepository<User>;
#[test]
fn test_repo() {
let mut mock_repo = MockRepository::<User>::new();
mock_repo.expect_find()
.with(predicate::eq(1))
.returning(|_| Some(User { id: 1, name: "Alice".into() }));
}
Para traits com lifetimes explícitos:
#[automock]
pub trait DataProcessor<'a> {
fn process(&self, data: &'a [u8]) -> Vec<u8>;
}
// O mock lida automaticamente com o lifetime
let mut mock = MockDataProcessor::new();
mock.expect_process()
.with(predicate::always())
.returning(|data: &[u8]| data.to_vec());
5. Testando Async Code com mockall
Mockall suporta traits assíncronos usando #[async_trait]:
use mockall::automock;
use async_trait::async_trait;
#[async_trait]
#[automock]
pub trait AsyncService {
async fn fetch_data(&self, id: u32) -> Result<String, String>;
}
#[tokio::test]
async fn test_async_service() {
let mut mock = MockAsyncService::new();
mock.expect_fetch_data()
.with(predicate::eq(42))
.returning(|_| Box::pin(async { Ok("data".to_string()) }));
let service = MyService::new(mock);
let result = service.get_data(42).await;
assert!(result.is_ok());
}
Note que funções async retornam Pin<Box<dyn Future>>, então precisamos encapsular o retorno em Box::pin.
6. Técnicas Avançadas de Mocking
Mock de métodos estáticos (structs sem traits)
mockall::mock! {
pub MyStruct {
fn static_method(x: i32) -> i32;
fn method(&self, x: i32) -> i32;
}
}
#[test]
fn test_static() {
let ctx = MockMyStruct::static_method_context();
ctx.expect()
.with(predicate::eq(10))
.returning(|x| x * 2);
assert_eq!(MockMyStruct::static_method(10), 20);
}
Verificação de ordem de chamadas
use mockall::Sequence;
#[test]
fn test_ordered_calls() {
let mut mock = MockDatabase::new();
let mut seq = Sequence::new();
mock.expect_get_user()
.times(1)
.in_sequence(&mut seq)
.returning(|_| Some("Alice".to_string()));
mock.expect_save_user()
.times(1)
.in_sequence(&mut seq)
.returning(|_, _| Ok(()));
}
Mock de múltiplas dependências
#[test]
fn test_multiple_deps() {
let mut mock_db = MockDatabase::new();
let mut mock_cache = MockCache::new();
mock_db.expect_get_user().returning(|_| Some("Alice".into()));
mock_cache.expect_get().returning(|_| None);
mock_cache.expect_set().returning(|_, _| ());
let service = UserService::new(mock_db, mock_cache);
assert_eq!(service.get_user_cached(1), Some("Alice".to_string()));
}
7. Boas Práticas e Padrões
Mantendo mocks simples
Evite expectativas excessivas — mocke apenas o necessário para o teste:
// Ruim: expectativas desnecessárias
mock.expect_get_user().returning(|_| None);
mock.expect_save_user().returning(|_, _| Ok(())); // nunca será chamada
// Bom: apenas o que o teste precisa
mock.expect_get_user().returning(|_| None);
Testando casos de erro
#[test]
fn test_error_case() {
let mut mock = MockDatabase::new();
mock.expect_save_user()
.returning(|_, _| Err("connection failed".to_string()));
let service = UserService::new(mock);
let result = service.create_user("Alice");
assert!(result.is_err());
}
Reutilizando mocks com fixtures
fn setup_mock_db() -> MockDatabase {
let mut mock = MockDatabase::new();
mock.expect_get_user()
.returning(|id| if id == 1 { Some("Alice".into()) } else { None });
mock
}
#[test]
fn test_with_fixture() {
let mock = setup_mock_db();
let service = UserService::new(mock);
assert_eq!(service.get_user(1), Some("Alice".into()));
}
8. Limitações e Alternativas
Limitações do mockall
- Não funciona com FFI (Foreign Function Interface)
- Pode ter problemas com macros complexas
- Traits com tipos associados complexos podem exigir configuração manual
Alternativas
- double: Biblioteca leve focada em simplicidade
- faux: Usa geração procedural para mocks
- injectable: Abordagem baseada em injeção de dependência
Quando usar injeção real vs mocking
Para testes de integração, considere usar implementações reais leves (como SQLite em memória) em vez de mocks. Mocking é ideal para testes unitários onde o comportamento da dependência precisa ser controlado precisamente.
Referências
- Documentação oficial do mockall — Guia completo com exemplos e referência da API
- Mockall no GitHub — Repositório oficial com issues, discussões e exemplos avançados
- Testing Rust Applications with Mockall — Tutorial prático cobrindo padrões comuns de teste
- Mocking in Rust: A Comprehensive Guide — Artigo aprofundado sobre estratégias de mocking em Rust
- Rust Async Testing with Mockall and Tokio — Guia específico para testes assíncronos combinando mockall e tokio
- Effective Rust Testing: Mocking Dependencies — Apresentação em vídeo sobre técnicas avançadas de mocking (link ilustrativo)