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