Pular para o conteúdo

Testes em Rust

Rust já vem com testes embutidos na própria linguagem e no cargo, então você não precisa instalar nenhuma biblioteca extra para começar. Esta página mostra o suficiente para você escrever e rodar seus primeiros testes.

Um teste é uma função comum marcada com #[test]:

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}

Algumas coisas que valem a pena notar:

  • #[cfg(test)] diz ao Rust que este módulo só é compilado quando você roda os testes. Ele não faz parte do seu programa normal.
  • use super::*; traz tudo o que existe no módulo pai (aqui, a função add), para que o teste consiga chamá-la.
  • #[test] marca uma função específica como um teste que o cargo test deve rodar.

Na pasta do seu projeto:

Terminal window
cargo test

Você vai ver algo assim:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

O Cargo escreve esse resumo em inglês: ele diz que rodou 1 teste, que it_adds_two_numbers deu certo (ok) e que houve 1 teste aprovado (passed), 0 com falha (failed) e 0 ignorados (ignored).

Três macros cobrem quase tudo o que você vai precisar:

#[test]
fn checks() {
assert!(2 + 2 == 4); // o teste falha se a condição for falsa
assert_eq!(2 + 2, 4); // falha se os dois valores não forem iguais
assert_ne!(2 + 2, 5); // falha se os dois valores FOREM iguais
}

Normalmente é melhor usar assert_eq! e assert_ne! do que um simples assert!(a == b). Quando eles falham, mostram os dois valores, e você enxerga na hora o que não bateu, em vez de ver só um “assertion failed” (a asserção falhou).

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_will_fail() {
assert_eq!(add(2, 2), 5);
}
}
test tests::this_will_fail ... FAILED
assertion `left == right` failed
left: 4
right: 5

Esse é exatamente o tipo de mensagem que você quer quando um teste falha. left (esquerda) é o valor que o seu código produziu e right (direita) é o valor que o teste esperava. Assim você vê de cara o que saiu e o que deveria ter saído.

Às vezes o comportamento correto é um panic (quando o programa para por causa de um erro). Por exemplo, uma função que deve recusar dados inválidos. Para testar isso existe o #[should_panic]:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("não é possível dividir por zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Quando um teste falha, você quer saber por quê o mais rápido possível. Dois pequenos acréscimos ajudam muito.

Uma mensagem personalizada. assert!, assert_eq! e assert_ne! aceitam uma mensagem extra depois dos valores, escrita do mesmo jeito que num println!. Ela só aparece se a verificação falhar.

expected no should_panic. Um #[should_panic] sozinho passa com qualquer panic, até com um causado por outro bug. Acrescentar expected = "..." faz o teste passar só se a mensagem do panic contiver aquele texto.

Aqui está de novo a função divide, agora com um main para você poder rodá-la, e dois testes que usam as duas ideias:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("não é possível dividir por zero");
}
a / b
}
fn main() {
println!("10 / 2 = {}", divide(10, 2));
println!("7 / 2 = {}", divide(7, 2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_down() {
let result = divide(7, 2);
assert_eq!(result, 3, "7 / 2 deveria arredondar para baixo, mas deu {result}");
}
#[test]
#[should_panic(expected = "dividir por zero")]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Saída:

10 / 2 = 5
7 / 2 = 3
▶ Experimente você mesmo

A saída acima é o que o cargo run mostra, porque o main não roda os testes. Para rodar os testes, use cargo test. No Rust Playground, abra o menu ao lado de Run e escolha Test. Você vai ver:

running 2 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

O should panic na segunda linha quer dizer que o teste esperava um panic, e ele aconteceu. Os testes rodam em paralelo, então as duas linhas test ... podem aparecer em qualquer ordem. Agora imagine que o teste esperasse 4 em vez de 3. A falha mostraria a sua mensagem, logo ao lado dos dois valores:

assertion `left == right` failed: 7 / 2 deveria arredondar para baixo, mas deu 3
left: 3
right: 4

E se a divide entrasse em panic com outra mensagem, a verificação do expected pegaria o problema. Por exemplo, com expected = "negativo", o Rust avisa que a mensagem do panic não continha o texto esperado (expected substring):

note: panic did not contain expected string
panic message: "não é possível dividir por zero"
expected substring: "negativo"

Sendo sincero: para um script curto, você provavelmente nem vai escrever testes. Os testes começam a compensar quando o código vai ser reutilizado ou alterado mais tarde. Eles deixam você reorganizar o código (refatorar) com confiança, porque o cargo test avisa na hora se você quebrou alguma coisa, em vez de você descobrir isso por um usuário.

Os testes também podem morar na sua documentação: exemplos de código dentro de comentários de documentação /// também rodam com cargo test, então os seus exemplos nunca ficam desatualizados. A página Documentação em Rust mostra como.

Nos dois exercícios, a solução tem um main que você pode rodar e testes que você roda com cargo test (ou com Test no Playground).

Escreva fn grade(score: u32) -> char, que devolve 'A' para 90 ou mais, 'B' de 80 a 89, 'C' de 70 a 79 e 'F' para qualquer nota abaixo disso. Depois escreva testes que verifiquem os limites, como 90 e 89. Dica: os limites são onde os bugs se escondem. Um teste com exatamente 90 pega o erro clássico de escrever > em vez de >=.

Ver solução
fn grade(score: u32) -> char {
if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else if score >= 70 {
'C'
} else {
'F'
}
}
fn main() {
for score in [95, 85, 72, 40] {
println!("{score} -> {}", grade(score));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edges_of_a() {
assert_eq!(grade(90), 'A');
assert_eq!(grade(89), 'B');
}
#[test]
fn edges_of_c() {
assert_eq!(grade(70), 'C');
assert_eq!(grade(69), 'F');
}
#[test]
fn zero_is_f() {
assert_eq!(grade(0), 'F', "uma nota 0 deveria ser reprovada");
}
}

Saída:

95 -> A
85 -> B
72 -> C
40 -> F
▶ Experimente você mesmo

O cargo test deve mostrar 3 passed (3 aprovados). Experimente trocar score >= 90 por score > 90 e rodar os testes de novo: o edges_of_a falha e mostra left: 'B' e right: 'A'.

Escreva fn safe_divide(a: i32, b: i32) -> Option<i32>, que devolve None quando b é 0, em vez de entrar em panic. Escreva um teste para uma divisão normal e outro para a divisão por zero. Dica: o assert_eq! também funciona com Option, então você pode escrever assert_eq!(safe_divide(1, 0), None).

Ver solução
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
println!("{:?}", safe_divide(10, 2));
println!("{:?}", safe_divide(1, 0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divides_normally() {
assert_eq!(safe_divide(10, 2), Some(5));
}
#[test]
fn zero_gives_none() {
assert_eq!(safe_divide(1, 0), None);
}
}

Saída:

Some(5)
None
▶ Experimente você mesmo

Compare com a função divide lá de cima. Nela, dividir por zero é um panic, então o teste precisa de #[should_panic]. Aqui é só um valor de retorno normal, então um simples assert_eq! resolve. Funções que devolvem Option ou Result costumam ser mais fáceis de testar.

  • Marque uma função com #[test] para transformá-la em teste e rode todos os testes com cargo test.
  • assert!, assert_eq! e assert_ne! cobrem quase todas as verificações do dia a dia. Prefira assert_eq!/assert_ne!, porque eles dão mensagens melhores quando falham.
  • #[should_panic] testa se uma função entra em panic quando deveria. Acrescente expected = "..." para conferir se o panic aconteceu pelo motivo certo.
  • Acrescente uma mensagem personalizada a qualquer macro de asserção para explicar o que deu errado quando ela falhar.
  • Testes valem mais no código que você pretende alterar ou reutilizar depois.

Teste o que aprendeu

1. Qual atributo marca uma função como um teste que o cargo test deve rodar?

2. Por que normalmente é melhor usar assert_eq! e assert_ne! do que um simples assert!(a == b)?

3. O que o #[should_panic] faz?

Pontuação: 0 / 3