Introdução
Testar código em Rust não é apenas boa prática — é parte fundamental da cultura da linguagem. O compilador já elimina classes inteiras de bugs (null pointers, data races, buffer overflows), mas lógica de negócio, integração entre módulos e edge cases continuam precisando de testes.
Neste guia, vamos cobrir desde testes unitários básicos até técnicas avançadas como property-based testing com proptest, snapshot testing e o uso do cargo-nextest como runner alternativo. Tudo com exemplos práticos prontos para copiar.
Testes Unitários com #[test]
O Rust tem suporte nativo a testes. Basta anotar uma função com #[test] e rodar cargo test:
pub fn fatorial(n: u64) -> u64 {
match n {
0 | 1 => 1,
_ => n * fatorial(n - 1),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fatorial_de_zero() {
assert_eq!(fatorial(0), 1);
}
#[test]
fn fatorial_de_cinco() {
assert_eq!(fatorial(5), 120);
}
#[test]
fn fatorial_de_dez() {
assert_eq!(fatorial(10), 3_628_800);
}
}
Convenções Importantes
O módulo #[cfg(test)] garante que o código de teste só é compilado durante cargo test, nunca no binário final. Isso é diferente de linguagens como Python ou Go, onde testes ficam em arquivos separados por convenção.
Boas práticas para nomes de testes:
#[cfg(test)]
mod tests {
use super::*;
// ✅ Bom: descreve cenário e resultado esperado
#[test]
fn deve_retornar_erro_quando_input_vazio() {
let resultado = processar("");
assert!(resultado.is_err());
}
// ❌ Ruim: nome genérico
#[test]
fn test1() {
assert!(processar("").is_err());
}
}
fn processar(input: &str) -> Result<String, String> {
if input.is_empty() {
Err("Input vazio".to_string())
} else {
Ok(input.to_uppercase())
}
}
Macros de Asserção
O Rust oferece várias macros para testes. Conheça as principais:
#[cfg(test)]
mod tests {
#[test]
fn demonstracao_asserts() {
// Igualdade
assert_eq!(2 + 2, 4);
assert_ne!(2 + 2, 5);
// Booleano
assert!(true);
assert!(!false);
// Com mensagem customizada
let valor = 42;
assert_eq!(
valor, 42,
"Esperava 42, mas recebeu {valor}"
);
}
#[test]
#[should_panic(expected = "divisão por zero")]
fn deve_fazer_panic_ao_dividir_por_zero() {
dividir(10, 0);
}
}
fn dividir(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("divisão por zero");
}
a / b
}
Para testes que retornam Result, uma abordagem mais elegante:
#[cfg(test)]
mod tests {
use std::num::ParseIntError;
#[test]
fn parse_numero_valido() -> Result<(), ParseIntError> {
let n: i32 = "42".parse()?;
assert_eq!(n, 42);
Ok(())
}
}
Essa abordagem é especialmente útil quando combinada com tratamento de erros via ? operator.
Testes de Integração
Testes de integração vivem na pasta tests/ na raiz do projeto e testam sua biblioteca como um consumidor externo:
meu-projeto/
├── src/
│ ├── lib.rs
│ └── parser.rs
├── tests/
│ ├── integracao_parser.rs
│ └── common/
│ └── mod.rs
└── Cargo.toml
// tests/common/mod.rs — helpers compartilhados
pub fn criar_fixture(conteudo: &str) -> tempfile::NamedTempFile {
use std::io::Write;
let mut arquivo = tempfile::NamedTempFile::new().unwrap();
write!(arquivo, "{}", conteudo).unwrap();
arquivo
}
// tests/integracao_parser.rs
mod common;
use meu_projeto::parser::Parser;
#[test]
fn parser_processa_arquivo_completo() {
let fixture = common::criar_fixture("chave = \"valor\"\noutro = 42");
let resultado = Parser::new()
.parse_arquivo(fixture.path())
.unwrap();
assert_eq!(resultado.get("chave"), Some(&"valor".to_string()));
assert_eq!(resultado.len(), 2);
}
#[test]
fn parser_retorna_erro_para_arquivo_invalido() {
let fixture = common::criar_fixture("isso não é TOML válido {{{");
let resultado = Parser::new().parse_arquivo(fixture.path());
assert!(resultado.is_err());
}
A pasta tests/common/ com mod.rs é o padrão idiomático para compartilhar fixtures entre testes de integração. Leia mais sobre organização de módulos em Rust.
Property-Based Testing com Proptest
Enquanto testes unitários verificam casos específicos, property-based testing gera centenas de inputs aleatórios para testar invariantes do seu código:
// Cargo.toml:
// [dev-dependencies]
// proptest = "1.10"
use proptest::prelude::*;
fn reverter(s: &str) -> String {
s.chars().rev().collect()
}
proptest! {
#[test]
fn reverter_duas_vezes_retorna_original(s in "\\PC*") {
let resultado = reverter(&reverter(&s));
prop_assert_eq!(&resultado, &s);
}
#[test]
fn reverter_preserva_tamanho(s in "\\PC*") {
prop_assert_eq!(reverter(&s).len(), s.len());
}
}
O proptest é poderoso para encontrar edge cases que você nunca pensaria em testar manualmente. Estratégias customizadas permitem gerar dados complexos:
use proptest::prelude::*;
#[derive(Debug, Clone)]
struct Pedido {
quantidade: u32,
preco_unitario: f64,
}
impl Pedido {
fn total(&self) -> f64 {
self.quantidade as f64 * self.preco_unitario
}
}
fn estrategia_pedido() -> impl Strategy<Value = Pedido> {
(1u32..1000, 0.01f64..10_000.0).prop_map(|(qtd, preco)| Pedido {
quantidade: qtd,
preco_unitario: preco,
})
}
proptest! {
#[test]
fn total_sempre_positivo(pedido in estrategia_pedido()) {
prop_assert!(pedido.total() > 0.0);
}
#[test]
fn total_proporcional_a_quantidade(
preco in 0.01f64..10_000.0,
qtd_a in 1u32..500,
qtd_b in 1u32..500,
) {
let pedido_a = Pedido { quantidade: qtd_a, preco_unitario: preco };
let pedido_b = Pedido { quantidade: qtd_b, preco_unitario: preco };
if qtd_a > qtd_b {
prop_assert!(pedido_a.total() > pedido_b.total());
}
}
}
cargo-nextest: Um Runner Mais Rápido
O cargo-nextest é um runner alternativo que executa cada teste em um processo separado, oferecendo melhor isolamento e relatórios mais claros:
# Instalar
cargo install cargo-nextest --locked
# Rodar testes (substitui cargo test)
cargo nextest run
# Com filtros
cargo nextest run -E 'test(parser)'
# Relatório JUnit para CI/CD
cargo nextest run --profile ci
Configure no .config/nextest.toml:
[profile.default]
retries = 2
slow-timeout = { period = "30s", terminate-after = 3 }
[profile.ci]
retries = 3
fail-fast = true
Isso é especialmente útil em pipelines de CI/CD onde testes flaky podem atrasar deploys.
Doc Tests: Documentação que Compila
Rust permite colocar testes diretamente na documentação, garantindo que exemplos nunca fiquem desatualizados:
/// Calcula a distância euclidiana entre dois pontos 2D.
///
/// # Exemplos
///
/// ```
/// use meu_crate::distancia;
///
/// let d = distancia((0.0, 0.0), (3.0, 4.0));
/// assert!((d - 5.0).abs() < f64::EPSILON);
/// ```
///
/// ```
/// use meu_crate::distancia;
///
/// // Mesma posição → distância zero
/// let d = distancia((1.0, 1.0), (1.0, 1.0));
/// assert_eq!(d, 0.0);
/// ```
pub fn distancia(a: (f64, f64), b: (f64, f64)) -> f64 {
let dx = b.0 - a.0;
let dy = b.1 - a.1;
(dx * dx + dy * dy).sqrt()
}
Esses exemplos rodam automaticamente com cargo test. Se a API mudar e o exemplo não compilar, o teste falha — documentação sempre atualizada.
Organização e Boas Práticas
Estrutura Recomendada
src/
├── lib.rs # Testes unitários inline (#[cfg(test)])
├── parser.rs # Testes unitários inline
└── utils.rs # Testes unitários inline
tests/
├── common/
│ └── mod.rs # Fixtures e helpers compartilhados
├── parser_test.rs # Testes de integração
└── api_test.rs # Testes end-to-end
benches/
└── parser_bench.rs # Benchmarks com Criterion
Checklist de Qualidade
- Nomes descritivos:
deve_retornar_none_quando_lista_vazia - Um assert por teste (quando possível) — facilita debugging
- Testes independentes: cada teste configura seu próprio estado
- Sem efeitos colaterais: use
tempfilepara arquivos, transactions para banco de dados - Rodar com
--releaseperiodicamente para pegar bugs de otimização - Cobertura: use
cargo tarpaulinoucargo llvm-covpara medir
# Cobertura com tarpaulin
cargo install cargo-tarpaulin
cargo tarpaulin --out html
# Cobertura com llvm-cov (mais preciso)
cargo install cargo-llvm-cov
cargo llvm-cov --html
Paralelismo e Performance
O cargo test roda testes em paralelo por padrão. Para controlar:
# Usar 4 threads
cargo test -- --test-threads=4
# Testes sequenciais (útil para testes com estado compartilhado)
cargo test -- --test-threads=1
Se você usa Rayon no código, cuidado: testes paralelos com Rayon podem consumir muitas threads. Considere usar rayon::ThreadPoolBuilder com pool dedicado nos testes.
Conclusão
A combinação de testes unitários nativos, integração com o Cargo, property-based testing com proptest e runners modernos como cargo-nextest faz do ecossistema de testes do Rust um dos mais completos entre linguagens de sistemas. Em Kotlin ou Go, o ferramental de testes também é excelente, mas a integração com o sistema de tipos do Rust oferece uma camada extra de confiança.
Comece com testes unitários, adicione integração para fluxos críticos e evolua para property-based testing em código com muitos edge cases. Seu futuro eu (e sua equipe) vai agradecer.
Leia também:
- Tratamento de Erros com thiserror e anyhow
- Rust 1.94: Novidades e Recursos Estabilizados
- CI/CD com Rust
- Proptest — Property-Based Testing
- Criterion — Benchmarks
- Tutoriais de Testes em Rust
Veja também nossos sites parceiros:
- Go Brasil — testes em Go também são excelentes, veja como se comparam
- Python Brasil Dev — pytest vs cargo test: abordagens diferentes, mesma filosofia
- Kotlin Brasil Dev — comparando ecossistemas de teste entre linguagens