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.
Escrevendo um teste simples
Seção intitulada “Escrevendo um teste simples”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çãoadd), para que o teste consiga chamá-la.#[test]marca uma função específica como um teste que ocargo testdeve rodar.
Rodando os testes
Seção intitulada “Rodando os testes”Na pasta do seu projeto:
cargo testVocê vai ver algo assim:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredO 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).
assert! e companhia
Seção intitulada “assert! e companhia”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).
Um teste que falha, e como ele aparece
Seção intitulada “Um teste que falha, e como ele aparece”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: 5Esse é 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.
Testando se uma função entra em panic
Seção intitulada “Testando se uma função entra em panic”À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); }}Mensagens de falha melhores
Seção intitulada “Mensagens de falha melhores”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 = 57 / 2 = 3A 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 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outO 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: 4E 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"Vale a pena escrever testes?
Seção intitulada “Vale a pena escrever testes?”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.
Prática
Seção intitulada “Prática”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).
1. Teste uma função de notas
Seção intitulada “1. Teste uma função de notas”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 -> A85 -> B72 -> C40 -> FO 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'.
2. Teste uma função que devolve um Option
Seção intitulada “2. Teste uma função que devolve um Option”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)NoneCompare 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 comcargo test. assert!,assert_eq!eassert_ne!cobrem quase todas as verificações do dia a dia. Prefiraassert_eq!/assert_ne!, porque eles dão mensagens melhores quando falham.#[should_panic]testa se uma função entra em panic quando deveria. Acrescenteexpected = "..."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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- How to Write Tests — The Rust Book (em inglês):
#[test], as macros de asserção, mensagens personalizadas,should_panice testes que devolvemResult. - Controlling How Tests Are Run — The Rust Book (em inglês): como rodar um teste só pelo nome, mostrar a saída do
println!e usar#[ignore]. - Test Organization — The Rust Book (em inglês): a diferença entre testes unitários e testes de integração numa pasta
tests/. - Unit testing — Rust by Example (em inglês): exemplos curtos de testes que passam, que falham e que entram em panic.
cargo test— The Cargo Book (em inglês): todas as opções do comandocargo test.