Testes em Rust
Nesta aula, você aprenderá como escrever e executar testes em Rust usando o atributo #[test], a macro assert! e o comando cargo test. Vamos explorar testes de unidade, boas práticas e exercícios práticos para consolidar o conhecimento.
Testes são uma parte fundamental do desenvolvimento de software, e Rust oferece suporte de primeira classe para escrever testes automatizados. Eles ajudam a garantir que seu código se comporte como esperado, facilitam a refatoração e aumentam a confiança na qualidade do software. Nesta aula, vamos explorar os conceitos básicos de testes em Rust: como escrever funções de teste com o atributo #[test], como usar a macro assert! para verificar condições, como executar os testes com o comando cargo test e como organizar testes de unidade dentro do seu projeto.
Além disso, veremos exemplos práticos e boas práticas para criar testes eficazes. Ao final, você terá uma base sólida para incorporar testes em seus projetos Rust, desde pequenas bibliotecas até aplicações complexas.
#[test]
Em Rust, o atributo #[test] é usado para marcar uma função como um teste. O compilador e o sistema de testes do Rust reconhecem essas funções e as executam quando você roda o comando cargo test. Uma função de teste é uma função comum que não recebe argumentos e não retorna nada (ou retorna um Result, como veremos mais adiante).
Para criar um teste, basta adicionar o atributo #[test] antes da definição da função. Por exemplo:
#[test]
fn test_soma() {
// código do teste
}
Você pode colocar testes em qualquer lugar do seu código, mas é comum colocá-los em um módulo tests dentro do mesmo arquivo ou em um arquivo separado na pasta tests/. O atributo #[test] só é compilado quando você executa os testes, então não há impacto no código de produção.
Se uma função de teste não for marcada com #[test], ela não será executada pelo cargo test. Além disso, o atributo #[test] pode ser combinado com outros atributos, como #[should_panic] para testar situações de erro.
Vamos ver um exemplo completo de uma função de teste simples:
fn soma(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soma() {
assert_eq!(soma(2, 3), 5);
}
}
Neste exemplo, o módulo tests é anotado com #[cfg(test)], o que faz com que ele seja compilado apenas durante a execução dos testes. A função test_soma verifica se a função soma retorna o valor esperado.
assert!
A macro assert! é usada para verificar se uma condição é verdadeira durante a execução do teste. Se a condição for falsa, a macro faz o teste falhar e exibe uma mensagem de erro. A sintaxe é:
assert!(expressão, "mensagem opcional", argumentos...);
A expressão deve ser uma condição booleana. Se ela for true, o teste continua; se for false, o teste falha e a mensagem (se fornecida) é exibida no console. Por exemplo:
#[test]
fn test_maioridade() {
let idade = 18;
assert!(idade >= 18, "A idade deve ser maior ou igual a 18, mas é {}", idade);
}
Existem variações da macro assert! que são muito úteis:
assert_eq!(a, b): verifica seaebsão iguais. Se não forem, exibe uma mensagem mostrando os valores deaeb.assert_ne!(a, b): verifica seaebsão diferentes.debug_assert!e variações: são como as macrosassert!, mas são compiladas apenas em modo debug (desenvolvimento). Elas são úteis para verificações que não devem impactar o desempenho em produção.
Exemplo com assert_eq!:
#[test]
fn test_multiplicacao() {
let resultado = 4 * 5;
assert_eq!(resultado, 20);
}
É importante usar assert_eq! e assert_ne! sempre que possível, pois eles fornecem mensagens de erro mais informativas, mostrando os valores reais e esperados.
cargo test
O comando cargo test é a forma padrão de executar todos os testes do seu projeto Rust. Ele compila o código (incluindo os testes) e executa cada função marcada com #[test]. Se algum teste falhar, o cargo exibe detalhes sobre a falha, como o nome do teste, a mensagem de erro e o local do erro.
Quando você executa cargo test, o cargo também executa testes de documentação (se houver), testes de integração (na pasta tests/) e testes de unidade dentro do código. O resultado é apresentado em uma saída resumida, com um contador de testes que passaram, falharam e foram ignorados.
Exemplo de saída:
running 2 tests
test tests::test_soma ... ok
test tests::test_multiplicacao ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Algumas opções úteis do cargo test:
cargo test nome_do_teste: executa apenas os testes que contêm a substringnome_do_testeno nome.cargo test -- --nocapture: exibe a saída deprintln!e outras macros de saída nos testes.cargo test -- --ignored: executa apenas os testes marcados com#[ignore].cargo test --release: executa os testes em modo release, com otimizações.
Para marcar um teste como ignorado, você pode usar o atributo #[ignore]:
#[test]
#[ignore]
fn teste_lento() {
// teste demorado
}
Isso é útil para testes que são lentos ou que dependem de recursos externos.
Testes de unidade
Testes de unidade são testes que verificam o comportamento de uma unidade específica de código, geralmente uma função ou um módulo. Em Rust, a prática comum é colocar testes de unidade no mesmo arquivo que o código, em um módulo tests anotado com #[cfg(test)]. Isso permite que os testes acessem funções privadas e internas do módulo, facilitando a verificação de detalhes de implementação.
Por exemplo, suponha que temos um módulo matematica com uma função privada dobro:
mod matematica {
fn dobro(x: i32) -> i32 {
x * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dobro() {
assert_eq!(dobro(5), 10);
}
}
}
Observe que usamos use super::*; para trazer as funções do módulo pai para o escopo do teste. Isso permite que os testes acessem funções privadas.
Além dos testes de unidade, o Rust também suporta testes de integração, que são colocados na pasta tests/ e testam o comportamento do código como uma biblioteca externa. No entanto, o foco desta aula são os testes de unidade.
Uma boa prática é escrever testes para cada função pública e, se necessário, para funções privadas que tenham lógica complexa. Os testes devem cobrir casos normais, casos de borda e casos de erro.
Exemplo de uma função com múltiplos testes:
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Divisão por zero"))
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dividir_sucesso() {
assert_eq!(dividir(10.0, 2.0), Ok(5.0));
}
#[test]
fn test_dividir_por_zero() {
assert_eq!(dividir(10.0, 0.0), Err(String::from("Divisão por zero")));
}
}
Neste exemplo, testamos tanto o caso de sucesso quanto o caso de erro, garantindo que a função se comporte corretamente em ambas as situações.
Boas práticas
Ao escrever testes, siga estas boas práticas:
- Escreva testes pequenos e focados: cada teste deve verificar um comportamento específico.
- Nomeie os testes de forma descritiva, indicando o que estão testando.
- Use
assert_eq!eassert_ne!em vez deassert!sempre que possível. - Teste casos de borda, como valores extremos, entradas vazias ou condições de erro.
- Mantenha os testes independentes: um teste não deve depender de outro para passar.
- Execute os testes com frequência para detectar regressões rapidamente.
Além disso, é importante lembrar que testes são parte do código e devem ser mantidos com o mesmo cuidado que o código de produção.
Exercícios
- Escreva uma função
eh_parque recebe um número inteiro e retornatruese for par,falsecaso contrário. Crie um teste de unidade para verificar o comportamento da função com números pares e ímpares. - Crie uma função
maiorque recebe dois números inteiros e retorna o maior. Escreva testes para verificar o comportamento com números positivos, negativos e iguais. - Implemente uma função
fatorialque calcula o fatorial de um número não negativo. Useassert!para verificar que a entrada é não negativa. Escreva testes para casos como 0, 1, 5 e 10. - Escreva uma função
inverter_stringque recebe uma string e retorna a string invertida. Crie testes de unidade para verificar a inversão de palavras e frases. - Utilize o atributo
#[should_panic]para escrever um teste que verifica que uma funçãodividir(que retornaf64diretamente, semResult) entra em pânico quando o divisor é zero. Implemente a função e o teste.
fn eh_par(n: i32) -> bool {
n % 2 == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eh_par() {
assert!(eh_par(4));
assert!(!eh_par(3));
}
}
fn maior(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_maior() {
assert_eq!(maior(5, 3), 5);
assert_eq!(maior(-2, -7), -2);
assert_eq!(maior(4, 4), 4);
}
}
fn fatorial(n: u64) -> u64 {
assert!(n >= 0, "Fatorial não definido para números negativos");
if n == 0 {
1
} else {
n * fatorial(n - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fatorial() {
assert_eq!(fatorial(0), 1);
assert_eq!(fatorial(1), 1);
assert_eq!(fatorial(5), 120);
assert_eq!(fatorial(10), 3628800);
}
}
fn inverter_string(s: &str) -> String {
s.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inverter_string() {
assert_eq!(inverter_string("hello"), "olleh");
assert_eq!(inverter_string("Rust"), "tsuR");
assert_eq!(inverter_string(""), "");
}
}
fn dividir(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Divisão por zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Divisão por zero")]
fn test_dividir_por_zero() {
dividir(10.0, 0.0);
}
}