Como escrever testes
Testes são funções Rust que verificam se o código que não é de teste está se comportando da forma esperada. Os corpos das funções de teste normalmente executam estas três ações:
- Configurar os dados ou o estado necessários.
- Executar o código que você quer testar.
- Verificar se os resultados são os esperados.
Vamos ver os recursos que Rust fornece especificamente para escrever testes que
sigam esse fluxo, incluindo o atributo test, algumas macros e o atributo
should_panic.
Estruturando funções de teste
Na forma mais simples, um teste em Rust é uma função anotada com o atributo
test. Atributos são metadados sobre partes do código Rust; um exemplo é o
atributo derive, que usamos com structs no Capítulo 5. Para transformar uma
função em uma função de teste, adicione #[test] na linha anterior a fn.
Quando você executa seus testes com o comando cargo test, Rust constrói um
binário executor de testes que executa as funções anotadas e informa se cada
função de teste passou ou falhou.
Sempre que criamos um novo projeto de biblioteca com Cargo, um módulo de testes com uma função de teste dentro dele é gerado automaticamente. Esse módulo oferece um modelo para escrever seus testes, para que você não precise relembrar a estrutura e a sintaxe exatas toda vez que iniciar um projeto novo. Você pode adicionar quantas funções de teste extras e quantos módulos de teste quiser!
Vamos explorar alguns aspectos de como os testes funcionam experimentando o modelo inicial antes de realmente testar qualquer código. Depois, escreveremos alguns testes mais realistas que chamam código escrito por nós e verificam se seu comportamento está correto.
Vamos criar um novo projeto de biblioteca chamado adder, que somará dois
números:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
O conteúdo do arquivo src/lib.rs na sua biblioteca adder deve se parecer
com a Listagem 11-1.
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
cargo newO arquivo começa com uma função de exemplo, add, para que tenhamos algo a
testar.
Por enquanto, vamos focar apenas na função it_works. Observe a anotação
#[test]: esse atributo indica que esta é uma função de teste, para que o
executor saiba tratá-la como tal. Também podemos ter funções que não são
testes dentro do módulo tests para ajudar a montar cenários comuns ou
executar operações repetidas, por isso sempre precisamos indicar quais funções
são testes.
O corpo da função de exemplo usa a macro assert_eq! para verificar que
result, que contém o resultado de chamar add com 2 e 2, é igual a 4. Essa
verificação serve como exemplo do formato de um teste típico. Vamos executá-lo
para confirmar que esse teste passa.
O comando cargo test executa todos os testes do nosso projeto, como mostra a
Listagem 11-2.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)
running 1 test
test tests::it_works ... 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
Cargo compilou e executou o teste. Vemos a linha running 1 test. A linha
seguinte mostra o nome da função de teste gerada, tests::it_works, e que o
resultado da execução desse teste é ok. O resumo geral test result: ok.
significa que todos os testes passaram, e a parte que diz 1 passed; 0 failed
totaliza quantos testes passaram ou falharam.
É possível marcar um teste como ignorado para que ele não seja executado em determinada ocasião; veremos isso mais adiante neste capítulo, na seção “Ignorando testes, a menos que sejam solicitados especificamente”
. Como ainda não fizemos isso aqui, o resumo mostra0 ignored. Também podemos passar um argumento ao comando cargo test para
executar somente os testes cujo nome corresponda a uma string; isso é chamado
de filtragem, e veremos esse recurso na seção
“Executando um subconjunto de testes por nome”. Aqui,
não filtramos os testes executados, então o fim do resumo mostra
0 filtered out.
A estatística 0 measured é usada para testes de benchmark que medem
desempenho. No momento em que este livro foi escrito, testes de benchmark só
estavam disponíveis no Rust noturno. Veja
a documentação sobre testes de benchmark para saber mais.
A próxima parte da saída, que começa em Doc-tests adder, corresponde aos
resultados de quaisquer testes de documentação. Ainda não temos testes de
documentação, mas Rust pode compilar qualquer exemplo de código que apareça na
nossa documentação de API. Esse recurso ajuda a manter documentação e código em
sincronia! Vamos discutir como escrever testes de documentação na seção
“Comentários de documentação como testes” do
Capítulo 14. Por enquanto, vamos ignorar a saída Doc-tests.
Vamos começar a adaptar o teste às nossas necessidades. Primeiro, altere o nome
da função it_works para outro nome, como exploration, assim:
Nome do arquivo: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Depois, execute cargo test novamente. Agora a saída mostra exploration em
vez de it_works:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... 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
Agora vamos adicionar outro teste, mas desta vez faremos um teste que falha!
Testes falham quando algo dentro da função de teste entra em pânico. Cada teste
é executado em uma nova thread, e quando a thread principal percebe que uma
thread de teste morreu, esse teste é marcado como falho. No Capítulo 9,
comentamos que a forma mais simples de provocar um pânico é chamar a macro
panic!. Digite o novo teste como uma função chamada another, para que seu
arquivo src/lib.rs fique como na Listagem 11-3.
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
panic!Execute os testes novamente com cargo test. A saída deve se parecer com a
Listagem 11-4, que mostra que nosso teste exploration passou e another
falhou.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Em vez de ok, a linha test tests::another mostra FAILED. Duas novas
seções aparecem entre os resultados individuais e o resumo. A primeira exibe o
motivo detalhado de cada falha de teste. Neste caso, vemos que
tests::another falhou porque entrou em pânico com a mensagem Make this test fail na linha 17 do arquivo src/lib.rs. A seção seguinte lista apenas os
nomes de todos os testes que falharam, o que é útil quando há muitos testes e
muitas saídas detalhadas de falha. Podemos usar o nome de um teste que falhou
para executá-lo isoladamente e depurá-lo com mais facilidade; falaremos mais
sobre formas de executar testes na seção
“Controlando como os testes são executados”
A linha de resumo aparece ao final: no geral, o resultado do nosso teste é
FAILED. Tivemos um teste que passou e um teste que falhou.
Agora que você já viu como os resultados de teste se apresentam em cenários
diferentes, vamos examinar algumas macros além de panic! que são úteis em
testes.
Verificando resultados com assert!
A macro assert!, fornecida pela biblioteca padrão, é útil quando você quer
garantir que alguma condição em um teste seja avaliada como true. Passamos à
macro assert! um argumento que é avaliado como um booleano. Se o valor for
true, nada acontece e o teste passa. Se o valor for false, a macro
assert! chama panic! para fazer o teste falhar. Usar a macro assert! nos
ajuda a verificar se o código está funcionando da maneira que pretendemos.
No Capítulo 5, na Listagem 5-15, usamos uma struct Rectangle e um método
can_hold, repetidos aqui na Listagem 11-5. Vamos colocar esse código no
arquivo src/lib.rs e, em seguida, escrever alguns testes para ele usando a
macro assert!.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Rectangle e seu método can_hold, do Capítulo 5O método can_hold retorna um booleano, o que o torna um caso de uso perfeito
para a macro assert!. Na Listagem 11-6, escrevemos um teste que exercita o
método can_hold criando uma instância de Rectangle com largura 8 e altura
7 e verificando que ela consegue conter outra instância de Rectangle com
largura 5 e altura 1.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
can_hold que verifica se um retângulo maior realmente consegue conter um retângulo menorObserve a linha use super::*; dentro do módulo tests. O módulo tests é um
módulo comum que segue as regras usuais de visibilidade que vimos no Capítulo 7
na seção
“Caminhos para se referir a um item na árvore de módulos”
para o escopo do módulo interno o código em teste definido no módulo externo.
Usamos um glob aqui, então tudo o que definirmos no módulo externo fica
disponível para esse módulo tests.
Chamamos nosso teste de larger_can_hold_smaller e criamos as duas instâncias
de Rectangle de que precisamos. Em seguida, chamamos a macro assert! e
passamos o resultado da chamada larger.can_hold(&smaller). Essa expressão
deve retornar true, então nosso teste deve passar. Vamos conferir!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Ele passa! Vamos adicionar outro teste, desta vez verificando que um retângulo menor não pode conter um retângulo maior:
Nome do arquivo: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --snip--
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Como o resultado correto da função can_hold nesse caso é false, precisamos
negar esse resultado antes de passá-lo à macro assert!. Como resultado, nosso
teste vai passar se can_hold retornar false:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Dois testes passando! Agora vamos ver o que acontece com os resultados quando
introduzimos um bug no nosso código. Vamos mudar a implementação do método
can_hold, substituindo o sinal de maior que (>) por um sinal de menor que
(<) na comparação das larguras:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Executando os testes agora, obtemos o seguinte:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Nossos testes detectaram o bug! Como larger.width é 8 e smaller.width é
5, a comparação das larguras em can_hold agora retorna false: 8 não é
menor que 5.
Testando igualdade com assert_eq! e assert_ne!
Uma forma comum de verificar funcionalidade é testar a igualdade entre o
resultado do código em teste e o valor que você espera que esse código retorne.
Você poderia fazer isso usando a macro assert! e passando a ela uma expressão
com o operador ==. Porém, isso é tão comum em testes que a biblioteca padrão
fornece um par de macros, assert_eq! e assert_ne!, para realizar essa
verificação de forma mais conveniente. Essas macros comparam dois argumentos
quanto à igualdade ou à desigualdade, respectivamente. Elas também imprimem os
dois valores quando a verificação falha, o que facilita entender por que o
teste falhou; em contraste, a macro assert! apenas indica que recebeu false
para a expressão com ==, sem mostrar os valores que levaram a esse resultado.
Na Listagem 11-7, escrevemos uma função chamada add_two, que soma 2 ao seu
parâmetro, e depois testamos essa função usando a macro assert_eq!.
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
add_two com a macro assert_eq!Vamos verificar se ela passa!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::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
Criamos uma variável chamada result que guarda o resultado de chamar
add_two(2). Em seguida, passamos result e 4 como argumentos para a macro
assert_eq!. A linha de saída desse teste é test tests::it_adds_two ... ok,
e o texto ok indica que nosso teste passou!
Vamos introduzir um bug no código para ver como assert_eq! se comporta quando
falha. Altere a implementação da função add_two para que ela some 3:
pub fn add_two(a: u64) -> u64 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
Execute os testes novamente:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Nosso teste detectou o bug! O teste tests::it_adds_two falhou, e a mensagem
nos informa que a verificação que falhou foi left == right e quais são os
valores left e right. Essa mensagem já nos ajuda a começar a depuração: o
argumento left, no qual tínhamos o resultado da chamada add_two(2), era
5, enquanto o argumento right era 4. Você pode imaginar como isso é
especialmente útil quando temos muitos testes em execução.
Observe que, em algumas linguagens e frameworks de teste, os parâmetros das
funções de verificação de igualdade são chamados de expected e actual, e a
ordem em que especificamos os argumentos importa. Em Rust, porém, eles se
chamam left e right, e a ordem em que fornecemos o valor esperado e o valor
produzido pelo código não importa. Poderíamos escrever a verificação desse
teste como assert_eq!(4, result), o que produziria a mesma mensagem de falha
que mostra assertion `left == right` failed.
A macro assert_ne! passa se os dois valores fornecidos forem diferentes e
falha se forem iguais. Ela é mais útil em casos em que não sabemos exatamente
qual valor será produzido, mas sabemos qual valor definitivamente não deve
ser. Por exemplo, se estivermos testando uma função que certamente modifica sua
entrada de alguma maneira, mas a forma dessa modificação depende do dia da
semana em que executamos os testes, talvez a melhor verificação seja afirmar
que a saída da função não é igual à entrada.
Por baixo dos panos, as macros assert_eq! e assert_ne! usam os operadores
== e !=, respectivamente. Quando as verificações falham, essas macros
imprimem seus argumentos usando formatação de depuração, o que significa que os
valores comparados precisam implementar as traits PartialEq e Debug. Todos
os tipos primitivos e a maior parte dos tipos da biblioteca padrão implementam
essas traits. Para structs e enums definidos por você, será necessário
implementar PartialEq para verificar igualdade entre esses tipos. Também será
necessário implementar Debug para imprimir os valores quando a verificação
falhar. Como ambas são traits deriváveis, como mencionado na Listagem 5-12 do
Capítulo 5, isso normalmente é tão simples quanto adicionar a anotação
#[derive(PartialEq, Debug)] à definição da sua struct ou enum. Veja o
Apêndice C, “Traits deriváveis”, para mais
detalhes sobre essas e outras traits deriváveis.
Adicionando mensagens de falha personalizadas
Você também pode adicionar uma mensagem personalizada para ser impressa junto
com a mensagem de falha como argumentos opcionais das macros assert!,
assert_eq! e assert_ne!. Todos os argumentos especificados depois dos
argumentos obrigatórios são repassados à macro format! (discutida em
“Concatenação com + ou format!”, no
Capítulo 8), então você pode fornecer uma string de formatação com espaços
reservados {} e os valores que devem preenchê-los. Mensagens personalizadas
são úteis para documentar o que a verificação quer dizer; quando um teste
falha, você terá uma noção melhor do que está errado no código.
Por exemplo, digamos que temos uma função que cumprimenta as pessoas pelo nome e queremos testar se o nome passado para a função aparece na saída:
Nome do arquivo: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {name}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
Os requisitos desse programa ainda não foram definidos por completo, e temos
quase certeza de que o texto Hello no começo da saudação vai mudar. Decidimos
que não queremos ter de atualizar o teste sempre que os requisitos mudarem; por
isso, em vez de verificar igualdade exata com o valor retornado por
greeting, vamos apenas verificar se a saída contém o texto do parâmetro de
entrada.
Agora vamos introduzir um bug nesse código, alterando greeting para excluir
name, para ver como é a mensagem de falha padrão:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
Executar esse teste produz o seguinte:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Esse resultado apenas indica que a verificação falhou e em qual linha ela
está. Uma mensagem de falha mais útil mostraria o valor retornado por
greeting. Vamos adicionar uma mensagem de falha personalizada, composta por
uma string de formatação com um espaço reservado preenchido pelo valor real que
obtivemos da função greeting:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
Agora, quando executarmos o teste, teremos uma mensagem de erro mais informativa:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Conseguimos ver o valor que realmente obtivemos na saída do teste, o que ajuda a depurar o que aconteceu, em vez de apenas o que esperávamos que tivesse acontecido.
Verificando pânicos com should_panic
Além de verificar valores de retorno, é importante confirmar se o código lida
com condições de erro da maneira esperada. Por exemplo, considere o tipo
Guess que criamos no Capítulo 9, na Listagem 9-13. Outro código que usa
Guess depende da garantia de que instâncias de Guess conterão apenas
valores entre 1 e 100. Podemos escrever um teste que garanta que tentar criar
uma instância de Guess com um valor fora desse intervalo provoque um pânico.
Fazemos isso adicionando o atributo should_panic à função de teste. O teste
passa se o código dentro da função entrar em pânico; o teste falha se o código
dentro da função não entrar em pânico.
A Listagem 11-8 mostra um teste que verifica se as condições de erro de
Guess::new acontecem quando esperamos.
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
panic!Colocamos o atributo #[should_panic] depois do atributo #[test] e antes da
função de teste à qual ele se aplica. Vamos ver o resultado quando esse teste
passa:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Parece bom! Agora vamos introduzir um bug em nosso código removendo a condição
de que a função new deve entrar em pânico quando o valor for maior que 100:
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
Quando executarmos o teste da Listagem 11-8, ele falhará:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Nesse caso, não recebemos uma mensagem muito útil, mas, olhando a função de
teste, vemos que ela está anotada com #[should_panic]. A falha que obtivemos
significa que o código dentro da função de teste não provocou pânico.
Testes que usam should_panic podem ser imprecisos. Um teste com
should_panic passa mesmo que o código entre em pânico por um motivo diferente
daquele que esperávamos. Para tornar esses testes mais precisos, podemos
adicionar um parâmetro opcional expected ao atributo should_panic. O
executor de testes verificará se a mensagem de falha contém o texto fornecido.
Por exemplo, considere o código modificado de Guess na Listagem 11-9, em que
a função new entra em pânico com mensagens diferentes dependendo de o valor
ser pequeno demais ou grande demais.
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
panic! com uma mensagem de pânico que contém uma substring especificadaEsse teste passará porque o valor que colocamos no parâmetro expected do
atributo should_panic é uma substring da mensagem com a qual a função
Guess::new entra em pânico. Poderíamos ter especificado a mensagem completa
de pânico esperada, que nesse caso seria Guess value must be less than or equal to 100, got 200. O que você escolhe especificar depende de quanto da
mensagem é único ou dinâmico e de quão preciso você quer que o teste seja.
Neste caso, uma substring da mensagem de pânico já basta para garantir que o
código da função de teste execute o caso else if value > 100.
Para ver o que acontece quando um teste com should_panic e mensagem
expected falha, vamos introduzir novamente um bug no nosso código trocando os
corpos dos blocos if value < 1 e else if value > 100:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Desta vez, quando executarmos o teste should_panic, ele falhará:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "Guess value must be greater than or equal to 1, got 200."
expected substring: "less than or equal to 100"
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
A mensagem de falha indica que esse teste realmente entrou em pânico como
esperávamos, mas a mensagem de pânico não continha a string esperada
less than or equal to 100. A mensagem de pânico que recebemos neste caso foi
Guess value must be greater than or equal to 1, got 200. Agora já podemos
começar a descobrir onde está o bug!
Usando Result<T, E> em testes
Até agora, todos os nossos testes entram em pânico quando falham. Também podemos
escrever testes que usem Result<T, E>! Aqui está o teste da Listagem 11-1,
reescrito para usar Result<T, E> e retornar um Err em vez de entrar em
pânico:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
A função it_works agora tem tipo de retorno Result<(), String>. No corpo da
função, em vez de chamar a macro assert_eq!, retornamos Ok(()) quando o
teste passa e um Err contendo uma String quando ele falha.
Escrever testes para que retornem um Result<T, E> permite usar o operador de
interrogação no corpo dos testes, o que pode ser uma maneira conveniente de
escrever testes que devem falhar se qualquer operação interna retornar uma
variante Err.
Você não pode usar a anotação #[should_panic] em testes que usam
Result<T, E>. Para verificar que uma operação retorna uma variante Err,
não use o operador de interrogação sobre o valor Result<T, E>. Em vez
disso, use assert!(value.is_err()).
Agora que você conhece várias maneiras de escrever testes, vamos ver o que
acontece quando executamos nossos testes e explorar as diferentes opções que
podemos usar com cargo test.