Testes em Rust: Estratégias e Boas Práticas — 2026

Guia completo de testes em Rust: unit tests, integration tests, property-based testing com proptest, snapshots, mocks e cargo-nextest para produtividade.

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

  1. Nomes descritivos: deve_retornar_none_quando_lista_vazia
  2. Um assert por teste (quando possível) — facilita debugging
  3. Testes independentes: cada teste configura seu próprio estado
  4. Sem efeitos colaterais: use tempfile para arquivos, transactions para banco de dados
  5. Rodar com --release periodicamente para pegar bugs de otimização
  6. Cobertura: use cargo tarpaulin ou cargo llvm-cov para 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:

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