Testes de Unidade em Rust

Descubra como organizar seu testes Rust com módulos

Quem tá chegando no Rust de outra linguagem pode até se confundir no começo, especialmente na hora de trabalhar com módulos e organização do código.

Os testes de unidade também são módulos no Rust, então tem um ou dois detalhes que você precisa saber pra evitar aquele problema frustrante de não conseguir ver os códigos no teste. Afinal, você só quer botar um teste simples no lugar certo, mas o Rust começa a dar erro falando que não acha coisa nenhuma! 😉

Nesse artigo, a gente vai descobrir o que causa esse problema e como mostrar pro Rust como você quer organizar os seus testes.

"Onde coloco esse arquivo?" Chega de drama com módulos em Rust

Como você já deve saber, em Rust seu projeto é organizado em módulos. É claro que a gente escreve o código em arquivos, mas o que é legal é que, diferente das outras linguagens (tipo C#, C++, Dart), aqui você não importa arquivos usando caminhos. No lugar disso, você faz um referência a outro módulo usando a palavra use. E a mesma organização vale para os testes de unidade também.

Se quiser saber mais sobre como estruturar seu projeto usando módulos, dá uma lida nesse artigo aqui.


Um teste de unidade simples

Olha só um exemplo tirado a dedo de teste de unidade em Rust:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

Quando usamos o #[cfg(test)], a gente tá falando pro Rust só compilar esse pedaço de código quando executar no terminal cargo test. Ou seja, o Rust vai ignorar essas linhas quando você compilar pra rodar o projeto normalmente (em modo debug ou pra release). Fica mais ligeiro pra compilar! E o atributo #[test] ali significa que essa função deve ser considerada como um teste na hora que o Rust for rodar tudo.

A parte seguinte é o módulo mod tests que vai agrupar os testes que você colocar embaixo dele. Como você já deve tá sacando, o módulo serve só pra organizar melhor o código. Mas você também pode colocar seus testes "soltos", sem ter módulo nenhum, ou escrever vários testes dentro de um grupo:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn it_works_too() {
        assert_eq!(2 + 2, 4);
    }
}

#[test]
fn it_works_outside() {
    assert_eq!(2 + 2, 4);
}

É só mandar um cargo test no seu projeto que a mágica acontece: todos os testes vão rodar!


Estruturação de Testes em Diretórios e Arquivos

Pelo que diz o Livro do Rust, a gente deve botar o teste de unidade logo no mesmo arquivo que o código sendo testado. Pode ser abaixo ou até em cima da parte que o programa vai usar quando rodar normalmente. O problema é que perde a visão geral do código e fica tudo misturado. E tem arquivos que podem ficar enormes com essa mistura!

Mas calma, o Rust te dá a liberdade de colocar seus testes num arquivo separado, e o código que vai rodar em outro. Pra fazer isso, você vai ter que dar uma olhada no sistema de módulos. Se entender certinho como funciona, a organização de arquivos e módulos vai dar bem menos trabalho.


Passo a Passo: Um módulo pra testar

Vamos imaginar que a gente criou uma estrutura simples com um único método privado. Essa SomeStructure fica dentro do módulo utils. A organização dos arquivos seria assim:

Diagrama 1

Reparou que a gente colocou os testes dentro de some_structure_tests.rs, em vez de botar tudo no mesmo arquivo. Mas antes de ver como ficaria, é melhor dar uma ligada no código "real". Olha o conteúdo do some_structure.rs:

pub struct SomeStructure {
    value: i32
}

impl SomeStructure {
    fn double_value(&self) -> i32 {
        self.value * 2
    }
}

fn module_private_function() {}
// --- some_structure.rs ---

E o arquivo some_structure_tests.rs:

#[cfg(test)]
mod some_structure_tests {
    use super::super::some_structure::{*};

    #[test]
    fn double_test() {
        let sut = SomeStructure { value: 2 };
        assert_eq!(sut.double_value(), 4);
    }

    #[test]
    fn module_private_test() {
        module_private_function();
        assert!(true)
    }
}
// --- some_structure_tests.rs ---

Como o Diagrama 1 mostra, esses arquivos estão dentro da pasta utils que corresponde ao módulo utils(se precisar revisar, dá uma olhada naquela parte do artigo sobre módulos). Aí a gente colocou os dois módulos, some_structure e some_structure_tests, exatamente como os arquivos que criamos dentro de utils:

#[cfg(test)]
mod some_structure_tests;

mod some_structure;
// --- utils.rs ---

Mas se a gente tentar compilar, vão aparecer estes erros:

❯ cargo test
   Compiling rust-unit-tests v0.1.0 (.../rust-unit-tests)
error[E0425]: cannot find function `module_private_function` in this scope
  --> src/utils/some_structure_tests.rs:13:9
   |
13 |         module_private_function();
   |         ^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope
   |
note: function `crate::utils::some_structure::module_private_function` exists but is inaccessible
  --> src/utils/some_structure.rs:11:1
   |
11 | fn module_private_function() {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not accessible

error[E0624]: method `double_value` is private
 --> src/utils/some_structure_tests.rs:8:24
  |
8 |         assert_eq!(sut.double_value(), 4);
  |                        ^^^^^^^^^^^^ private method
  |
 ::: src/utils/some_structure.rs:6:5
  |
6 |     fn double_value(&self) -> i32 {
  |     ----------------------------- private method defined here

Some errors have detailed explanations: E0425, E0624.
For more information about an error, try `rustc --explain E0425`.
error: could not compile `rust-unit-tests` (bin "rust-unit-tests" test) due to 2 previous errors

O Rust reclama que não consegue "enxergar" os elementos privados do módulo some_structure quando vai rodar o teste. Ou seja: a gente ainda tem mais um problema pra resolver!

Quando o teste está no mesmo módulo que o código normal, ele pode acessar os métodos e funções privadas sem problema. Só que a gente moveu o teste pra outro arquivo, e isso quebrou essa conexão. Agora precisamos colocar ela no lugar de novo. Pra fazer isso, o Rust tem uma ferramenta de submódulos, onde o submódulo consegue acessar as partes privadas do móduloprincipal dele.

Pra dizer que queremos que o módulo some_structure_tests (que está dentro de some_structure_tests.rs) seja um submódulo do some_structure mesmo, precisamos fazer uma mudança no arquivo some_structure.rs – e não no utils.rs. A primeira coisa é arrumar o utils.rs tirando a linha que fala do some_structure_tests:

mod some_structure;
// --- utils.rs ---

Então, vamos adicionar as duas linhas que retiramos no arquivo some_structure, para transformá-lo em um submódulo:

#[cfg(test)]
mod some_structure_tests;

// [...]
// --- some_structure.rs ---

A hierarquia dos módulos mudou. Anteriormente, o some_structure_tests era um submódulo do utils. Agora, ele é um submódulo do some_structure. Para mostrar essa mudança na hierarquia, é preciso ajustar o "caminho use" (use-path) que importa/acessa as partes do some_structure dentro dos testes de unidade:

#[cfg(test)]
mod some_structure_tests {
    // vamos ajustar o caminho para:
    use super::super::{*};

    // [...]
}
// --- some_structure_tests.rs ---

Quando tentarmos compilar o projeto, o Rust vai reclamar dizendo que não encontra o arquivo onde definimos o módulo some_structure_tests:

error[E0583]: file not found for module `some_structure_tests`
 --> src/utils/some_structure.rs:3:1
  |
3 | mod some_structure_tests;
  | ^^^^^^^^^^^^^^^^^^^^^^^^^
  |

O Rust está procurando por uma pasta some_structure_tests, exatamente como ele conecta o módulo utils dentro de utils.rs com a pasta utils. Para mostrar o caminho correto ao Rust, precisamos indicar explicitamente a localização. Para isso, vamos usar o atributo #[path] e modificar o arquivo some_structure.rs novamente:

#[cfg(test)]
#[path = "some_structure_tests.rs"]
mod some_structure_tests;

// [...]
// --- some_structure.rs ---

Tudo certo, agora é hora do cargo test mostrar seu trabalho! Se tudo estiver configurado direito, os testes devem passar tranquilamente!

cargo test
   Compiling rust-unit-tests v0.1.0 (.../rust-unit-tests)
    Finished test [unoptimized + debuginfo] target(s) in 0.34s
     Running unittests src/main.rs (target/debug/deps/rust_unit_tests-53005ba7bbdc82f2)

running 2 tests
test utils::some_structure::some_structure_tests::some_structure_tests::double_test ... ok
test utils::some_structure::some_structure_tests::some_structure_tests::module_private_test ... ok

Resumindo

Vamos dar uma olhada no Diagrama 2 para ver a organização final dos módulos e arquivos:

Diagrama 2

Pra resumir, sempre que você quiser separar os testes de unidade do código de produção (some_structure), basta criar um submódulo (some_structure_tests) no arquivo Rust onde fica o módulo que será testado (some_structure.rs). Esse submódulo pode apontar para o arquivo dedicado aos testes (some_structure_tests.rs) usando o atributo #[path]. Essa técnica preserva a capacidade de acessar os elementos privados de um módulo, enquanto permite organizar os testes em arquivos do jeito que a gente quiser.

Se você já trabalhava com outras linguagens, onde geralmente há um arquivo só pros testes, pode ser que o Rust pareça meio estranho no começo – já que nem no Livro do Rust eles explicam como organizar os testes e o código de produção fora de um único arquivo. No meu caso, eu perdi um tempinho até entender a lógica.

Espero que minhas descobertas nesse artigo ajudem a entender melhor como lidar com testes de unidade em Rust.

https://github.com/tiagonevestia/rust-unit-tests


Referências

How To Structure Unit Tests in Rust - Artigo Original

Test Organization

Did you find this article valuable?

Support Tiago Neves by becoming a sponsor. Any amount is appreciated!