Nesta aula, vamos explorar duas ferramentas essenciais para garantir a qualidade do seu código Rust: os testes de integração e os doc tests. Enquanto os testes unitários verificam partes isoladas do código, os testes de integração validam a interação entre módulos e o comportamento externo da sua biblioteca ou binário. Já os doc tests permitem que exemplos contidos na documentação sejam executados como testes, garantindo que a documentação nunca fique desatualizada.

Vamos começar entendendo a estrutura de pastas que o Rust espera para testes de integração e, em seguida, mergulharemos nos doc tests, que são uma prática poderosa para manter a documentação viva. Ao final, discutiremos como organizar seus testes e como obter uma visão geral da cobertura de código.

Pasta tests/

Em um projeto Rust, a pasta tests/ é um local especial onde você coloca testes de integração. Diferente dos testes unitários (que ficam dentro dos arquivos de código-fonte, geralmente em um módulo #[cfg(test)]), os testes de integração são arquivos .rs separados que estão fora da pasta src/. Cada arquivo dentro de tests/ é compilado como um crate separado e pode acessar a sua biblioteca como se fosse um usuário externo.

Para criar um teste de integração, basta criar um arquivo dentro de tests/. Por exemplo, se você tem uma biblioteca chamada my_lib, pode criar tests/integration.rs e escrever:

// tests/integration.rs
use my_lib::add;

#[test]
fn test_add() {
    assert_eq!(add(2, 3), 5);
}

Para que isso funcione, sua biblioteca precisa expor os itens publicamente (com pub). Os testes de integração são executados com cargo test e são ideais para testar o comportamento da API pública, cenários de uso real e interações entre módulos.

Uma vantagem dos testes de integração é que eles podem testar a sua crate como um consumidor externo, garantindo que a interface pública seja utilizável e que os módulos internos não vazem detalhes desnecessários. Isso ajuda a manter um design limpo e a detectar problemas que os testes unitários podem não pegar.

Doc tests

Os doc tests são testes escritos dentro da documentação do seu código, usando a sintaxe de blocos de código Markdown com a linguagem especificada como rust. Quando você executa cargo test, o Rust também compila e executa esses exemplos como testes. Isso garante que os exemplos na documentação estão corretos e funcionam como esperado.

Para criar um doc test, basta adicionar um bloco de código em um comentário de documentação (///) ou em um comentário de módulo (//!). Por exemplo:

/// Adiciona dois números.
///
/// # Exemplos
///
/// ```
/// use my_lib::add;
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

No exemplo acima, o bloco de código dentro da documentação é um doc test. O Rust irá compilá-lo e executá-lo, verificando que as afirmações são verdadeiras. Você também pode usar a anotação ```rust,ignore para ignorar um bloco, ou ```rust,no_run para compilar mas não executar.

Os doc tests são uma forma excelente de manter a documentação viva e precisa. Eles incentivam a escrever exemplos que realmente funcionam, e qualquer alteração no código que quebre um exemplo será detectada nos testes. Isso é especialmente útil para bibliotecas públicas, onde a documentação é a principal forma de os usuários aprenderem a usar o código.

Organização

Conforme seu projeto cresce, é importante organizar os testes de forma consistente. Para testes de integração, você pode criar subpastas dentro de tests/ para agrupar testes por funcionalidade. Por exemplo:

tests/
├── common/
│   └── mod.rs
├── integration.rs
└── api/
    └── test_auth.rs

O arquivo common/mod.rs pode conter funções auxiliares compartilhadas entre vários testes de integração. Para usá-lo, você pode declarar mod common; em cada arquivo de teste. Lembre-se de que cada arquivo em tests/ é um crate separado, então você precisa incluir os módulos explicitamente.

Para testes unitários, a organização tradicional é colocar um módulo tests dentro de cada arquivo de código-fonte, ou em um arquivo src/tests.rs que é incluído com #[cfg(test)] mod tests;. Isso mantém os testes próximos do código que estão testando, facilitando a manutenção.

Outra prática comum é usar a ferramenta cargo test com filtros para executar apenas um subconjunto de testes, por exemplo cargo test test_add para rodar apenas o teste com nome contendo test_add. Isso ajuda a depurar rapidamente um teste específico.

Cobertura (visão geral)

A cobertura de código é uma métrica que indica qual porcentagem do seu código é exercitada pelos testes. Embora o Rust não tenha uma ferramenta de cobertura embutida, existem ferramentas externas como tarpaulin (para Linux) ou cargo-llvm-cov (que usa a instrumentação LLVM). Essas ferramentas geram relatórios detalhados mostrando quais linhas, funções e branches foram executados durante os testes.

Para instalar o cargo-llvm-cov, você pode usar cargo install cargo-llvm-cov e depois executar cargo llvm-cov no seu projeto. Ele coleta a cobertura de todos os testes, incluindo unitários, integração e doc tests. Exemplo de uso:

cargo llvm-cov --open

Esse comando gera um relatório em HTML e o abre no navegador. Você pode ver quais partes do código não foram testadas e adicionar testes para melhorar a cobertura.

Uma cobertura alta não garante a ausência de bugs, mas é um indicador útil de que seu código está sendo exercitado. É importante lembrar que a cobertura deve ser usada como um guia, não como um objetivo cego. Priorize testar os caminhos críticos e os casos de borda.

Boas práticas

Sempre que possível, escreva doc tests para funções públicas, pois eles servem como documentação viva e testes ao mesmo tempo. Para testes de integração, foque em cenários de uso real e na interação entre módulos. Mantenha os testes organizados e use nomes descritivos. Além disso, execute os testes com frequência e monitore a cobertura para identificar lacunas.

Referências

Exercícios

  1. Crie uma biblioteca simples chamada calculator com uma função pública add(a: i32, b: i32) -> i32 e um teste de integração em tests/ que verifique a soma de dois números.

    ✓ Resposta: Crie o projeto com cargo new calculator --lib. Em src/lib.rs, defina pub fn add(a: i32, b: i32) -> i32 { a + b }. Crie tests/integration.rs com:
    use calculator::add;
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
    
  2. Escreva um doc test para a função add do exercício anterior, garantindo que o exemplo na documentação é executado e passa.

    ✓ Resposta: Adicione um comentário de documentação em src/lib.rs antes da função:
    /// Soma dois números.
    ///
    /// # Exemplos
    ///
    /// ```
    /// use calculator::add;
    /// let result = add(2, 3);
    /// assert_eq!(result, 5);
    /// ```
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
  3. Organize seus testes de integração em subpastas: crie tests/api/mod.rs com um teste e tests/common/mod.rs com uma função utilitária. Use essa função no teste.

    ✓ Resposta: Estrutura:
    tests/
    ├── api/
    │   └── mod.rs
    └── common/
        └── mod.rs
    
    Em tests/common/mod.rs:
    pub fn setup() -> i32 { 42 }
    
    Em tests/api/mod.rs:
    use calculator::add;
    mod common;
    #[test]
    fn test_add_with_common() {
        let x = common::setup();
        assert_eq!(add(x, 1), 43);
    }
    
  4. Execute cargo test e observe a saída. Identifique quantos testes foram executados e se todos passaram. Em seguida, adicione um teste que falhe de propósito e veja o relatório.

    ✓ Resposta: A saída do cargo test mostrará algo como:
    running 1 test
    test test_add ... ok
    
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
    
    Para um teste que falha, basta usar assert_eq!(add(2, 3), 6); e o cargo test indicará a falha com a mensagem de pânico.
  5. Instale o cargo-llvm-cov e gere um relatório de cobertura para o seu projeto. Identifique pelo menos uma função ou linha que não está coberta e escreva um teste para cobri-la.

    ✓ Resposta: Instale com cargo install cargo-llvm-cov e execute cargo llvm-cov --open. O relatório mostrará a porcentagem de cobertura. Por exemplo, se a função add estiver coberta, mas outra função subtract não, adicione um teste para ela.