Introdução
Bem-vindo(a) à Test Organization. Este laboratório faz parte do Rust Book. Você pode praticar suas habilidades em Rust no LabEx.
Neste laboratório, aprenderemos sobre as duas principais categorias de testes na comunidade Rust: testes unitários (unit tests), que são pequenos e focados em testar módulos individuais em isolamento, e testes de integração (integration tests), que usam a interface pública da biblioteca e potencialmente exercitam múltiplos módulos por teste.
Organização de Testes
Como mencionado no início do capítulo, testes são uma disciplina complexa, e diferentes pessoas usam diferentes terminologias e organizações. A comunidade Rust pensa sobre testes em termos de duas categorias principais: testes unitários (unit tests) e testes de integração (integration tests). Testes unitários são pequenos e mais focados, testando um módulo em isolamento por vez, e podem testar interfaces privadas. Testes de integração são inteiramente externos à sua biblioteca e usam seu código da mesma forma que qualquer outro código externo faria, usando apenas a interface pública e potencialmente exercitando múltiplos módulos por teste.
Escrever ambos os tipos de testes é importante para garantir que as partes da sua biblioteca estão fazendo o que você espera que façam, separadamente e em conjunto.
Testes Unitários
O objetivo dos testes unitários é testar cada unidade de código em isolamento do restante do código para identificar rapidamente onde o código está e não está funcionando como esperado. Você colocará os testes unitários no diretório src em cada arquivo com o código que eles estão testando. A convenção é criar um módulo chamado tests em cada arquivo para conter as funções de teste e anotar o módulo com cfg(test).
O Módulo de Testes e #[cfg(test)]
A anotação #[cfg(test)] no módulo tests diz ao Rust para compilar e executar o código de teste somente quando você executa cargo test, e não quando você executa cargo build. Isso economiza tempo de compilação quando você só quer construir a biblioteca e economiza espaço no artefato compilado resultante porque os testes não são incluídos. Você verá que, como os testes de integração vão em um diretório diferente, eles não precisam da anotação #[cfg(test)]. No entanto, como os testes unitários vão nos mesmos arquivos que o código, você usará #[cfg(test)] para especificar que eles não devem ser incluídos no resultado compilado.
Lembre-se que quando geramos o novo projeto adder na primeira seção deste capítulo, o Cargo gerou este código para nós:
Nome do arquivo: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
Este código é o módulo tests gerado automaticamente. O atributo cfg significa configuração e diz ao Rust que o seguinte item deve ser incluído apenas dada uma determinada opção de configuração. Neste caso, a opção de configuração é test, que é fornecida pelo Rust para compilar e executar testes. Ao usar o atributo cfg, o Cargo compila nosso código de teste somente se executarmos ativamente os testes com cargo test. Isso inclui quaisquer funções auxiliares que possam estar dentro deste módulo, além das funções anotadas com #[test].
Testando Funções Privadas
Há debate dentro da comunidade de testes sobre se as funções privadas devem ou não ser testadas diretamente, e outras linguagens tornam difícil ou impossível testar funções privadas. Independentemente da ideologia de teste que você segue, as regras de privacidade do Rust permitem que você teste funções privadas. Considere o código na Listagem 11-12 com a função privada internal_adder.
Nome do arquivo: src/lib.rs
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
Listagem 11-12: Testando uma função privada
Observe que a função internal_adder não está marcada como pub. Os testes são apenas código Rust, e o módulo tests é apenas outro módulo. Como discutimos em "Caminhos para Referenciar um Item na Árvore de Módulos", itens em módulos filhos podem usar os itens em seus módulos ancestrais. Neste teste, trazemos todos os itens do pai do módulo test para o escopo com use super::*, e então o teste pode chamar internal_adder. Se você não acha que as funções privadas devem ser testadas, não há nada no Rust que o obrigue a fazê-lo.
Testes de Integração
No Rust, os testes de integração são inteiramente externos à sua biblioteca. Eles usam sua biblioteca da mesma forma que qualquer outro código usaria, o que significa que eles só podem chamar funções que fazem parte da API pública da sua biblioteca. Seu objetivo é testar se muitas partes da sua biblioteca funcionam corretamente em conjunto. Unidades de código que funcionam corretamente por conta própria podem ter problemas quando integradas, por isso a cobertura de teste do código integrado também é importante. Para criar testes de integração, você primeiro precisa de um diretório tests.
O Diretório de Testes
Criamos um diretório tests no nível superior do diretório do nosso projeto, ao lado de src. O Cargo sabe procurar arquivos de teste de integração neste diretório. Podemos então criar quantos arquivos de teste quisermos, e o Cargo compilará cada um dos arquivos como um crate individual.
Vamos criar um teste de integração. Com o código na Listagem 11-12 ainda no arquivo src/lib.rs, crie um diretório tests e crie um novo arquivo chamado tests/integration_test.rs. Sua estrutura de diretórios deve ser assim:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Insira o código na Listagem 11-13 no arquivo tests/integration_test.rs.
Nome do arquivo: tests/integration_test.rs
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
Listagem 11-13: Um teste de integração de uma função no crate adder
Cada arquivo no diretório tests é um crate separado, então precisamos trazer nossa biblioteca para o escopo de cada crate de teste. Por essa razão, adicionamos use adder; no topo do código, o que não precisávamos nos testes unitários.
Não precisamos anotar nenhum código em tests/integration_test.rs com #[cfg(test)]. O Cargo trata o diretório tests de forma especial e compila arquivos neste diretório somente quando executamos cargo test. Execute cargo test agora:
[object Object]
As três seções da saída incluem os testes unitários, o teste de integração e os testes de documentação. Observe que se algum teste em uma seção falhar, as seções seguintes não serão executadas. Por exemplo, se um teste unitário falhar, não haverá nenhuma saída para testes de integração e documentação, porque esses testes só serão executados se todos os testes unitários passarem.
A primeira seção para os testes unitários [1] é a mesma que temos visto: uma linha para cada teste unitário (um chamado internal que adicionamos na Listagem 11-12) e, em seguida, uma linha de resumo para os testes unitários.
A seção de testes de integração começa com a linha Running tests/integration_test.rs [2]. Em seguida, há uma linha para cada função de teste nesse teste de integração [3] e uma linha de resumo para os resultados do teste de integração [4] logo antes da seção Doc-tests adder começar.
Cada arquivo de teste de integração tem sua própria seção, então, se adicionarmos mais arquivos no diretório tests, haverá mais seções de teste de integração.
Ainda podemos executar uma função de teste de integração específica especificando o nome da função de teste como um argumento para cargo test. Para executar todos os testes em um arquivo de teste de integração específico, use o argumento --test do cargo test seguido pelo nome do arquivo:
[object Object]
Este comando executa apenas os testes no arquivo tests/integration_test.rs.
Submódulos em Testes de Integração
À medida que você adiciona mais testes de integração, pode querer criar mais arquivos no diretório tests para ajudar a organizá-los; por exemplo, você pode agrupar as funções de teste pela funcionalidade que estão testando. Como mencionado anteriormente, cada arquivo no diretório tests é compilado como seu próprio crate separado, o que é útil para criar escopos separados para imitar mais de perto a maneira como os usuários finais usarão seu crate. No entanto, isso significa que os arquivos no diretório tests não compartilham o mesmo comportamento que os arquivos em src compartilham, como você aprendeu no Capítulo 7 sobre como separar o código em módulos e arquivos.
O comportamento diferente dos arquivos do diretório tests é mais perceptível quando você tem um conjunto de funções auxiliares para usar em vários arquivos de teste de integração e tenta seguir as etapas em "Separando Módulos em Arquivos Diferentes" para extraí-los em um módulo comum. Por exemplo, se criarmos tests/common.rs e colocarmos uma função chamada setup nele, podemos adicionar algum código a setup que queremos chamar de várias funções de teste em vários arquivos de teste:
Nome do arquivo: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
Quando executarmos os testes novamente, veremos uma nova seção na saída do teste para o arquivo common.rs, embora este arquivo não contenha nenhuma função de teste nem tenhamos chamado a função setup de nenhum lugar:
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-
92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
Running tests/integration_test.rs
(target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
Ter common aparecendo nos resultados do teste com running 0 tests exibido para ele não é o que queríamos. Queríamos apenas compartilhar algum código com os outros arquivos de teste de integração. Para evitar que common apareça na saída do teste, em vez de criar tests/common.rs, criaremos tests/common/mod.rs. O diretório do projeto agora se parece com isto:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
Esta é a convenção de nomenclatura mais antiga que o Rust também entende, que mencionamos em "Caminhos de Arquivos Alternativos". Nomear o arquivo dessa forma diz ao Rust para não tratar o módulo common como um arquivo de teste de integração. Quando movemos o código da função setup para tests/common/mod.rs e excluímos o arquivo tests/common.rs, a seção na saída do teste não aparecerá mais. Arquivos em subdiretórios do diretório tests não são compilados como crates separados ou têm seções na saída do teste.
Depois de criarmos tests/common/mod.rs, podemos usá-lo de qualquer um dos arquivos de teste de integração como um módulo. Aqui está um exemplo de como chamar a função setup do teste it_adds_two em tests/integration_test.rs:
Nome do arquivo: tests/integration_test.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
Observe que a declaração mod common; é a mesma da declaração de módulo que demonstramos na Listagem 7-21. Então, na função de teste, podemos chamar a função common::setup().
Testes de Integração para Crates Binários
Se nosso projeto for um crate binário que contém apenas um arquivo src/main.rs e não tiver um arquivo src/lib.rs, não podemos criar testes de integração no diretório tests e trazer funções definidas no arquivo src/main.rs para o escopo com uma instrução use. Apenas os crates de biblioteca expõem funções que outros crates podem usar; crates binários são destinados a serem executados por conta própria.
Esta é uma das razões pelas quais os projetos Rust que fornecem um binário têm um arquivo src/main.rs simples que chama a lógica que reside no arquivo src/lib.rs. Usando essa estrutura, os testes de integração podem testar o crate de biblioteca com use para disponibilizar a funcionalidade importante. Se a funcionalidade importante funcionar, a pequena quantidade de código no arquivo src/main.rs também funcionará, e essa pequena quantidade de código não precisa ser testada.
Resumo
Parabéns! Você concluiu o laboratório de Organização de Testes. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.