Testes unitários e de integração no Rust
1. Fundamentos dos testes em Rust
O Rust possui um sistema de testes integrado à linguagem e ao gerenciador de pacotes Cargo. O atributo #[test] marca uma função como teste, e a macro assert! verifica se uma condição é verdadeira:
#[test]
fn test_soma() {
let resultado = 2 + 2;
assert!(resultado == 4);
}
Além de assert!, temos as macros especializadas assert_eq! (igualdade) e assert_ne! (diferença):
#[test]
fn test_multiplicacao() {
assert_eq!(3 * 4, 12);
assert_ne!(10 / 3, 3); // divisão inteira: 10/3 = 3, mas 3 != 3.333...
}
Para executar os testes, use cargo test. Flags úteis incluem -- --nocapture (exibe saída padrão) e --test nome (executa apenas testes específicos):
cargo test -- --nocapture
cargo test test_multiplicacao
2. Testes unitários: testando componentes isolados
Testes unitários são escritos no mesmo arquivo que o código testado, dentro de um módulo tests com #[cfg(test)]:
pub fn adicionar_um(x: i32) -> i32 {
x + 1
}
fn funcao_privada(x: i32) -> i32 {
x * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adicionar_um() {
assert_eq!(adicionar_um(5), 6);
}
#[test]
fn test_funcao_privada() {
assert_eq!(funcao_privada(3), 6);
}
}
O módulo tests só é compilado durante testes (#[cfg(test)]). Como está no mesmo escopo, pode acessar funções privadas. Boas práticas incluem: nomear testes descritivamente, manter um assert por teste e focar em comportamentos específicos.
3. Testes de integração: testando a API pública
Testes de integração ficam no diretório tests/ na raiz do projeto. Cada arquivo .rs nesse diretório é um crate separado que testa a API pública:
// tests/test_integracao.rs
use meu_projeto::adicionar_um;
#[test]
fn test_integracao_adicionar_um() {
assert_eq!(adicionar_um(10), 11);
}
Para compartilhar código entre testes de integração, crie tests/common/mod.rs:
// tests/common/mod.rs
pub fn setup() {
// inicialização comum
}
// tests/test_integracao.rs
mod common;
#[test]
fn test_com_setup() {
common::setup();
// teste propriamente dito
}
4. Organização avançada e escopos de teste
Para projetos com binários, os testes de integração só podem acessar a biblioteca (lib.rs), não o binário principal (main.rs). A estrutura recomendada é:
meu_projeto/
├── src/
│ ├── lib.rs # API pública
│ └── main.rs # usa a lib
└── tests/
└── test_integracao.rs
Testes unitários são ideais para verificar lógica interna e funções auxiliares. Testes de integração validam o comportamento do sistema como um todo através da interface pública.
5. Testes com should_panic e Result
Para testar que uma função deve causar pânico, use #[should_panic]:
#[test]
#[should_panic(expected = "divisão por zero")]
fn test_divisao_por_zero() {
let _ = 10 / 0;
}
Alternativamente, retorne Result<(), E> para usar o operador ?:
#[test]
fn test_com_result() -> Result<(), String> {
let resultado = "42".parse::<i32>().map_err(|e| format!("Erro: {}", e))?;
assert_eq!(resultado, 42);
Ok(())
}
6. Testes parametrizados e fixtures
Para criar fixtures, use funções auxiliares e OnceCell para inicialização única:
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn obter_config() -> &'static Config {
CONFIG.get_or_init(|| Config::carregar())
}
#[test]
fn test_com_config() {
let config = obter_config();
assert!(config.porta > 0);
}
Testes parametrizados podem ser feitos com loops:
#[test]
fn test_varios_casos() {
let casos = vec![(2, 4), (3, 9), (4, 16)];
for (entrada, esperado) in casos {
assert_eq!(entrada * entrada, esperado);
}
}
Para testes mais avançados, use a crate rstest:
use rstest::rstest;
#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 4)]
fn test_quadrado(#[case] entrada: i32, #[case] esperado: i32) {
assert_eq!(entrada * entrada, esperado);
}
7. Testes de documentação com rustdoc
O Rust permite testar exemplos na documentação usando doc tests:
/// Soma dois números.
///
/// # Exemplos
///
/// ```
/// use meu_projeto::soma;
///
/// assert_eq!(soma(2, 3), 5);
/// ```
pub fn soma(a: i32, b: i32) -> i32 {
a + b
}
Execute apenas doc tests com cargo test --doc. Eles garantem que a documentação permaneça atualizada e funcionando.
8. Integração contínua e ferramentas complementares
Para CI com GitHub Actions:
# .github/workflows/rust.yml
name: Rust
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo test --all-features
Para cobertura de código, use cargo-tarpaulin:
cargo install cargo-tarpaulin
cargo tarpaulin --out Html
Execução seletiva: cargo test filtro executa testes cujo nome contenha "filtro". Use #[ignore] para pular testes:
#[test]
#[ignore = "requer banco de dados"]
fn test_lento() {
// teste demorado
}
Testes são parte fundamental do ecossistema Rust. Eles garantem qualidade, servem como documentação viva e facilitam refatorações seguras. Comece com testes unitários simples e evolua para testes de integração e doc tests conforme seu projeto cresce.
Referências
- The Rust Programming Language - Testing Chapter — Capítulo oficial sobre testes no livro de Rust, cobrindo unitários, integração e organização.
- Rust by Example - Testing — Exemplos práticos de testes unitários, de integração e doc tests.
- cargo-tarpaulin Documentation — Ferramenta de cobertura de código para Rust, compatível com CI.
- rstest crate documentation — Crate para testes parametrizados com atributos
#[rstest]e#[case]. - Rust API Guidelines - Testing — Diretrizes oficiais para escrever testes em bibliotecas Rust.
- The Cargo Book - Tests — Documentação oficial do Cargo sobre execução e configuração de testes.
- Rust Doc Tests Guide — Guia completo sobre testes de documentação com
rustdoc.