O Modelo de Testes do Rust
Toda linguagem eventualmente precisa de uma resposta para "onde os testes vivem e o que eles veem", e o Rust responde a essa pergunta de forma incomumente direta, incorporando um test harness no próprio cargo.
Não há um equivalente a escolher um runner ou configurar um arquivo de configuração antes que o primeiro teste compile.
Em vez disso, a linguagem oferece três lugares distintos para colocar código de teste, cada um com uma relação diferente com os internos do seu crate, e entender por que esses três lugares existem é a chave para ler e organizar qualquer suíte de testes do Rust que você encontrar.
Esta página é a âncora conceitual para o restante da seção de testes: ela explica o harness em si, a fronteira unitária/de integração e por que os doctests são tratados como um tipo de teste de primeira classe em vez de uma reflexão tardia da documentação.
Resumo
- O test harness do Rust organiza os testes por o que eles podem ver, não apenas por onde os arquivos estão no disco.
- Por que Importa: Sem uma divisão baseada em visibilidade, as suítes de testes tendem a testar internos privados que os consumidores nunca tocam, ou nunca exercitam o contrato público que realmente é enviado.
- Conceitos Chave: test harness,
#[cfg(test)], testes unitários, testes de integração, doctests, unidade de compilação. - Quando Usar: Opte por testes unitários ao verificar a lógica interna, testes de integração ao verificar a API pública como um consumidor a usaria, e doctests quando um exemplo em sua documentação precisa permanecer comprovadamente correto.
- Limitações / Trade-offs: O harness integrado é opinativo e mínimo; ele não suporta nativamente fixtures, testes parametrizados ou relatórios ricos como algumas frameworks de outros ecossistemas.
- Tópicos Relacionados: organização de testes, mocking e test doubles, cobertura e CI, testes baseados em propriedades.
Fundamentos
Um test harness é o programa que descobre funções de teste, as executa e relata os resultados de sucesso/falha.
Na maioria dos ecossistemas, esta é uma biblioteca separada que você adiciona como dependência, mas no Rust, a biblioteca padrão e o cargo fornecem um harness juntos, então cargo test funciona em um projeto recém-criado sem configuração.
Qualquer função anotada com #[test] se torna um caso de teste, e o harness compila um binário de teste especial que executa todas as funções #[test] descobertas, capturando a saída e relatando falhas com a asserção que foi acionada.
A maneira mais simples de pensar sobre isso é que cargo test não executa o seu binário ou biblioteca como está; ele constrói uma versão alternativa do seu código com um scaffolding de teste compilado, executa essa versão em vez disso e descarta o scaffolding depois.
Essa compilação alternativa é controlada pelo atributo #[cfg(test)], que diz ao compilador "inclua este código apenas ao compilar para testes".
Por causa do cfg(test), o código de teste não adiciona nenhum tamanho nem custo de tempo de execução ao binário que você realmente envia, o que é uma escolha de design deliberada: testes são uma preocupação em tempo de desenvolvimento, não uma dependência de produção.
Um exemplo mínimo mostra o formato que todo desenvolvedor Rust eventualmente memoriza:
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*; // puxa add() do módulo pai
#[test]
fn adds_two_positive_numbers() {
assert_eq!(add(2, 2), 4);
}
}O bloco mod tests fica ao lado do código que ele exercita, e use super::* é o que lhe dá acesso a itens privados que o mundo exterior nunca pode ver.
Esse único detalhe, acesso privado de dentro do crate, é a semente que cresce para toda a divisão unitária/de integração abordada a seguir.
Mecânicas e Interações
O harness traça sua linha mais importante em torno das unidades de compilação: o que é compilado junto, e portanto o que pode ver o quê.
Um módulo #[cfg(test)] dentro de src/ é compilado como parte do mesmo crate que o código que ele testa, então ele tem acesso total a funções privadas, campos privados e tipos não exportados.
Código sob tests/, por outro lado, é compilado como um conjunto de crates totalmente separados, um por arquivo, cada um dos quais depende da sua biblioteca da mesma forma que um consumidor externo faria: através de use my_crate::... e apenas os itens marcados como pub.
Esta não é uma convenção de pasta arbitrária; é o mecanismo que o Rust usa para fazer uma promessa: um teste de integração nunca pode passar acidentalmente alcançando a API pública, porque o compilador simplesmente não o deixará ver mais nada.
Essa restrição tem uma consequência direta sobre para que cada tipo de teste é bom. Testes unitários respondem "este algoritmo interno se comporta corretamente", enquanto testes de integração respondem "a coisa que eu exporto se comporta corretamente quando usada como um chamador real a usaria".
my_crate/
├── src/
│ ├── lib.rs # #[cfg(test)] mods vivem ao lado da lógica
│ └── parser.rs
└── tests/
├── api_smoke.rs # crate separado, vê apenas itens `pub`
└── cli_behavior.rs # crate separado, compilado independentemente
Doctests ocupam uma terceira posição, estruturalmente diferente. Um doctest é um bloco de código embutido em um comentário de documentação ///, e rustdoc extrai cada bloco desse tipo, o envolve em um fn main() oculto, e o compila e executa como seu próprio pequeno programa durante cargo test.
Doctests sempre exercitam apenas itens públicos, porque eles estão literalmente ilustrando como um chamador escreveria use my_crate::thing; e então o usaria, então eles compartilham a visibilidade de API pública apenas dos testes de integração, mas servem a um propósito diferente: provar que o exemplo em sua documentação ainda compila e ainda produz o resultado que o texto afirma.
A armadilha que a maioria dos novatos encontra é assumir que esses três tipos de testes são intercambiáveis, quando na verdade cada um falha em capturar uma categoria diferente de bug. Uma suíte de testes unitários sem testes de integração pode passar enquanto a API pública é inutilizável; uma suíte de integração sem doctests pode passar enquanto cada exemplo de código em sua saída cargo doc está desatualizado e incorreto.
Considerações Avançadas e Aplicações
Em escala, a divisão em três vias se torna uma ferramenta organizacional tanto quanto uma técnica. Grandes crates frequentemente empurram quase toda a lógica para funções pequenas e puras cobertas por testes unitários, mantêm um conjunto enxuto de testes de integração em torno do punhado de pontos de entrada públicos reais, e confiam em doctests especificamente para os exemplos que aparecem em docs.rs.
Essa divisão também interage com o desempenho da compilação de uma forma que surpreende pessoas de outras linguagens: como cada arquivo em tests/ compila como seu próprio crate, e cada um se vincula à sua biblioteca completa, um diretório tests/ com dezenas de arquivos pode diminuir notavelmente o tempo de cargo test em comparação com a mesma contagem de testes expressa como testes unitários, pois o trabalho do linker é repetido por arquivo.
Doctests também carregam seu próprio custo operacional. Eles rodam mais lentamente por teste do que testes unitários porque cada exemplo de documentação é iniciado como um programa compilado independente, o que é uma razão pela qual crates grandes às vezes marcam exemplos caros como no_run (compilados, mas não executados) em vez de deixá-los totalmente ativos.
A divisão em três tipos também molda como os pipelines de CI são estruturados: cargo test --lib isola testes unitários para um sinal rápido de loop interno, cargo test --test <nome> executa um arquivo de integração por vez, e cargo test --doc executa os exemplos de documentação como seu próprio portão, permitindo que as equipes ajustem qual sinal bloqueia uma mesclagem versus qual é executado noturnamente.
Onde as equipes vão além do harness integrado são testes baseados em propriedades e testes de snapshot, que não são novos locais de teste, mas novas estratégias de asserção sobrepostas ao mesmo mecanismo #[test], e ferramentas de cobertura, que instrumentam os mesmos três tipos de teste em vez de substituí-los.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
Testes unitários (#[cfg(test)]) | Rápido, vê internos privados, barato de executar em massa | Pode passar enquanto a API pública está quebrada | Lógica pura, parsers, funções com muitos casos de borda |
Testes de integração (tests/) | Espelha o uso real do consumidor, captura bugs de conexão | Compilação mais lenta, só vê a superfície pub | Manipuladores HTTP, pontos de entrada CLI, conexão entre módulos |
| Doctests | Garante que os exemplos na documentação realmente compilam | Mais lento por teste, inconveniente para async/configuração complexa | Exemplos de uso da API pública em docs.rs |
Equívocos Comuns
- "O Rust precisa de um framework de testes como outras linguagens." - O harness
#[test]vem com a toolchain; frameworks comoproptestoucriterionadicionam capacidades por cima, não substituem o mecanismo base. - "
tests/é apenas uma convenção de nomenclatura, como uma pastatest/em outros ecossistemas." - É um limite de compilação imposto pelo compilador: cada arquivo lá é seu próprio crate que só pode ver sua API pública, não meramente um diretório que o harness escaneia por acaso. - "Doctests são um enfeite opcional da documentação." - São testes reais, compilados e executados que falham sua compilação tanto quanto qualquer outro; eles existem especificamente para que exemplos desatualizados não sejam enviados silenciosamente.
- "Testes unitários e testes de integração são intercambiáveis; escolha o que for mais conveniente." - Eles respondem a perguntas diferentes (correção interna versus correção de contrato público), então uma suíte dominada por um tipo tem um ponto cego real e estrutural.
- "Mais arquivos em
tests/significa melhor organização." - Cada arquivo é um crate separado que relinka sua biblioteca, então dividir testes de integração de forma muito fina pode tornarcargo testmais lento sem adicionar valor de cobertura.
FAQs
O que é o test harness do Rust, em uma frase?
É o mecanismo integrado, fornecido com cargo e a biblioteca padrão, que descobre funções anotadas com #[test], compila um binário de teste especial e relata resultados de sucesso/falha sem nenhuma dependência externa.
Por que o Rust inclui testes na toolchain em vez de deixá-los para crates de terceiros?
- Remove um ponto de decisão para cada novo projeto:
cargo testfunciona imediatamente. - Mantém um vocabulário base consistente (
#[test],assert_eq!) em todo o ecossistema. - Crates de terceiros ainda adicionam valor por cima (testes de propriedade, snapshots, benchmarking) sem precisar reinventar a descoberta de testes.
Como o cargo test decide o que conta como um teste?
Ele escaneia por funções anotadas com #[test] dentro do código compilado sob #[cfg(test)], compila um binário de teste dedicado contendo-as, então compila e executa separadamente cada arquivo sob tests/ e cada exemplo de documentação, executando todos os três passes em uma única invocação.
Como os módulos #[cfg(test)] veem funções privadas quando os arquivos tests/ não podem?
Um #[cfg(test)] mod faz parte da mesma unidade de compilação que o código ao seu redor, então as regras normais de visibilidade do Rust se aplicam e use super::* alcança itens privados; um arquivo tests/*.rs é compilado como um crate independente que depende do seu como qualquer consumidor externo, então apenas itens pub são visíveis para ele.
Um doctest é um teste "real" ou apenas uma conveniência de documentação?
É um teste real, compilado e executado independentemente; rustdoc extrai cada bloco de código delimitado por crase de um comentário ///, o envolve em uma função oculta, e cargo test falha a compilação se esse código não compilar ou suas asserções não passarem.
Quando devo escrever um teste de integração em vez de um teste unitário?
Quando você quer verificar o comportamento da maneira que um usuário downstream real o experimentaria, como chamar uma função pública, acessar um manipulador HTTP ou invocar um CLI, já que testes de integração só exercitam o que é realmente exportado.
Qual é o trade-off de depender apenas de testes unitários?
Testes unitários podem todos passar enquanto a superfície pública do crate está quebrada ou inutilizável, porque eles nunca passam pelo mesmo caminho use my_crate::... que um consumidor real leva; um pequeno conjunto de testes de integração fecha essa lacuna.
Qual é o trade-off de depender apenas de testes de integração?
Eles compilam mais lentamente (cada arquivo é seu próprio crate vinculado à sua biblioteca completa) e não podem ver os internos privados, tornando mais difícil identificar casos de borda profundos dentro de um algoritmo sem exercitar todo o caminho público ao redor dele.
Doctests substituem a necessidade de testes de integração?
Não; doctests existem para manter os exemplos de documentação honestos, não para fornecer cobertura completa, e geralmente são mantidos curtos e ilustrativos em vez de exaustivos.
Por que doctests às vezes usam no_run em vez de realmente executar?
Alguns exemplos são caros, precisam de acesso à rede ou teriam efeitos colaterais inadequados para uma execução de teste, então no_run diz ao rustdoc para compilar o exemplo (capturando erros de sintaxe e tipo) sem executá-lo.
A divisão em três vias (unitário, integração, doctest) afeta os tempos de compilação?
Sim: testes unitários compilam uma vez como parte da compilação de teste do crate, mas cada arquivo tests/*.rs é um crate separado que relinka a biblioteca completa, e cada doctest compila como seu próprio pequeno programa, então testes de integração e de documentação geralmente adicionam mais tempo de relógio por teste do que testes unitários.
Posso executar apenas um dos três tipos de teste?
Sim: cargo test --lib executa apenas testes unitários, cargo test --test <nome> executa um arquivo de integração, e cargo test --doc executa apenas doctests, o que é comumente usado para dar a cada tipo seu próprio estágio de CI.
Relacionados
- Noções Básicas de Testes - um guia prático de
#[test], asserções ecargo test - Testes Unitários vs. Integração - comparação mais aprofundada das duas localizações e quando usar cada uma
- Doctests - receita completa para escrever e controlar exemplos de documentação
- Organização de Testes - estruturando fixtures e helpers compartilhados em uma suíte
- Melhores Práticas de Testes - padrões em nível de equipe para a pirâmide de testes
Versões de Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.