Curso gratuito de Rust: A linguagem mais amada
Testes em Rust: Escrevendo Testes Unitários
Aprenda sobre testes em rust: escrevendo testes unitários
Testes em Rust: Escrevendo Testes Unitários
Olá, futuro mestre em Rust! 👋 Nesta aula prática, vamos mergulhar no mundo dos testes unitários em Rust, uma ferramenta essencial para garantir a robustez e a confiabilidade do seu código. Rust é uma linguagem que incentiva fortemente a escrita de testes, e você verá como é fácil e intuitivo fazê-lo.
1. Introdução aos Testes Unitários em Rust ✨
Testes unitários são pequenos pedaços de código que verificam se unidades individuais de código (como funções ou métodos) se comportam como esperado. Em Rust, eles são uma parte fundamental do processo de desenvolvimento, ajudando a prevenir bugs, refatorar com confiança e documentar o comportamento do código.
O Rust fornece um sistema de teste integrado que é fácil de usar e muito eficiente. Você não precisa de bibliotecas externas complexas para começar a testar; tudo o que você precisa já vem com o compilador e o gerenciador de pacotes cargo.
Nesta aula, você aprenderá a:
- Escrever testes unitários básicos usando
#[test]. - Utilizar macros de asserção como
assert!,assert_eq!eassert_ne!. - Testar condições de pânico com
#[should_panic]. - Testar funções que retornam
Result<T, E>. - Executar seus testes com
cargo test.
Vamos começar a codificar! 🚀
2. Explicação Detalhada com Exemplos 🧪
2.1. A Estrutura Básica de um Teste Unitário
Em Rust, os testes são funções anotadas com o atributo #[test]. Quando você executa cargo test, o Rust encontra e executa essas funções.
A convenção é colocar os testes unitários no mesmo arquivo que o código que eles estão testando, dentro de um módulo tests aninhado. Isso permite que os testes acessem itens privados do módulo externo, o que é útil para testar a lógica interna.
Vamos criar um novo projeto para nossos exemplos:
cargo new rust_tests_aula
cd rust_tests_aulaAgora, edite o arquivo src/lib.rs (ou src/main.rs se for um executável, mas lib.rs é mais comum para bibliotecas que serão testadas por outras partes).
// src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)] // Esta linha garante que o módulo 'tests' só seja compilado durante os testes
mod tests {
use super::*; // Importa tudo do módulo pai (neste caso, `add_two`)
#[test] // Atributo que marca a função como um teste
fn it_adds_two() {
assert_eq!(4, add_two(2)); // Verifica se 4 é igual ao resultado de add_two(2)
}
#[test]
fn another_test() {
assert_ne!(5, add_two(2)); // Verifica se 5 NÃO é igual ao resultado de add_two(2)
}
#[test]
fn basic_assertion() {
let result = add_two(3);
assert!(result > 4, "Resultado esperado maior que 4, mas foi {}", result); // Verifica uma condição booleana
}
}Explicação:
pub fn add_two(a: i32) -> i32: Uma função pública que queremos testar.#[cfg(test)]: Esta anotação condicional significa que o módulotestssó será compilado quando você executarcargo test. Isso economiza espaço no binário final compilado para produção.mod tests { ... }: Define um módulo para nossos testes.use super::*;: Importa todas as definições do módulo pai (olib.rsprincipal) para dentro do módulotests. Isso nos permite usaradd_twodiretamente.#[test]: Marca a funçãoit_adds_twocomo uma função de teste.assert_eq!(expected, actual): Macro que verifica se dois valores são iguais. Se não forem, o teste falha e imprime os dois valores.assert_ne!(expected, actual): Macro que verifica se dois valores são diferentes. Se forem iguais, o teste falha.assert!(boolean_expression, "Mensagem de erro"): Macro que verifica se uma expressão booleana é verdadeira. Se for falsa, o teste falha e a mensagem de erro opcional é exibida.
Para executar esses testes, salve o arquivo e execute no terminal:
cargo testVocê deverá ver uma saída similar a esta:
running 3 tests
test tests::another_test ... ok
test tests::basic_assertion ... ok
test tests::it_adds_two ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
2.2. Testando Condições de Pânico com #[should_panic]
Às vezes, queremos garantir que nosso código entre em pânico sob certas condições de erro. Para isso, usamos o atributo #[should_panic].
Você pode até especificar uma mensagem esperada para o pânico com expected = "mensagem". Isso é útil para garantir que o pânico ocorra pelo motivo certo.
Vamos adicionar uma função que pode entrar em pânico e um teste para ela em src/lib.rs:
// src/lib.rs (continue do exemplo anterior)
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::*;
// ... testes anteriores ...
#[test]
#[should_panic] // Este teste espera que a função `new` entre em pânico
fn greater_than_100_panics() {
Guess::new(200);
}
#[test]
#[should_panic(expected = "Guess value must be between 1 and 100, got 0.")] // Espera uma mensagem de pânico específica
fn less_than_1_panics_with_message() {
Guess::new(0);
}
}Execute cargo test novamente. Você verá que os novos testes de pânico também são bem-sucedidos, pois eles esperam o pânico. Se a função não entrasse em pânico, o teste #[should_panic] falharia.
2.3. Testando Funções que Retornam Result<T, E>
Testar funções que retornam Result<T, E> é um pouco diferente. Você pode usar a sintaxe ? dentro de um teste se o teste também retornar Result<(), E>. Isso permite que você propague erros de forma concisa.
Vamos adicionar uma função que retorna Result e um teste para ela em src/lib.rs:
// src/lib.rs (continue do exemplo anterior)
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum MyError {
InputTooSmall,
InputTooLarge,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::InputTooSmall => write!(f, "Input is too small!"),
MyError::InputTooLarge => write!(f, "Input is too large!"),
}
}
}
pub fn check_value(value: i32) -> Result<String, MyError> {
if value < 10 {
Err(MyError::InputTooSmall)
} else if value > 100 {
Err(MyError::InputTooLarge)
} else {
Ok(format!("Value {} is within range.", value))
}
}
#[cfg(test)]
mod tests {
use super::*;
// ... testes anteriores ...
#[test]
fn check_value_in_range_ok() -> Result<(), MyError> { // O teste agora retorna Result
let result = check_value(50)?; // Usa '?' para propagar o erro se houver
assert_eq!(result, "Value 50 is within range.".to_string());
Ok(()) // Retorna Ok(()) se o teste for bem-sucedido
}
#[test]
fn check_value_too_small_err() {
let result = check_value(5);
assert!(result.is_err()); // Verifica se o resultado é um erro
assert_eq!(result.unwrap_err(), MyError::InputTooSmall); // Desempacota o erro e verifica o tipo
}
#[test]
fn check_value_too_large_err() {
let result = check_value(150);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MyError::InputTooLarge);
}
}Observações sobre o teste de Result:
fn check_value_in_range_ok() -> Result<(), MyError>: O teste agora retorna umResult. Isso permite que você use o operador?.let result = check_value(50)?;: Secheck_value(50)retornar umErr, o?fará com que o teste retorne imediatamente esseErr, falhando o teste. Se retornarOk, ele desempacota o valor.Ok(()): Se todas as asserções passarem e nenhum erro for propagado, o teste retornaOk(()), indicando sucesso.- Para testar casos de erro, você pode usar
is_err()eunwrap_err()para acessar o erro e verificar seu tipo ou mensagem.
Execute cargo test novamente para ver todos os testes passando.
2.4. Executando Testes Específicos e Opções do cargo test
O cargo test é muito versátil. Aqui estão algumas opções úteis:
-
Executar todos os testes:
cargo test -
Executar um teste específico (pelo nome):
cargo test check_value_in_range_okVocê pode usar partes do nome também:
cargo test panicsIsso executaria todos os testes que contêm "panics" no nome, como
greater_than_100_panicseless_than_1_panics_with_message. -
Mostrar a saída de
println!nos testes: Por padrão, os testes em Rust capturam a saída deprintln!. Se você quiser ver essa saída (útil para depuração), use a flag-- --nocapture:cargo test -- --nocapture(Note o
--que separa as opções docargodas opções do executor de testes). -
Executar testes sequencialmente (não em paralelo): Por padrão,
cargo testexecuta os testes em paralelo. Se você tiver testes que dependem de estado compartilhado ou recursos externos, pode ser necessário executá-los um por um:cargo test -- --test-threads=1 -
Ignorar testes: Você pode marcar testes para serem ignorados com
#[ignore]. Isso é útil para testes caros ou que ainda estão em desenvolvimento.// src/lib.rs // ... #[cfg(test)] mod tests { // ... #[test] #[ignore] // Este teste será ignorado por padrão fn expensive_test() { // Este teste leva muito tempo para executar assert!(true); } }Para executar os testes ignorados, use
-- --ignored:cargo test -- --ignoredPara executar todos os testes, incluindo os ignorados:
cargo test -- --include-ignored
3. Exercícios/Desafios Práticos ✅
Agora é a sua vez de colocar a mão na massa! Crie um novo projeto Rust e implemente as seguintes funcionalidades, escrevendo testes unitários para cada uma delas.
Crie um novo projeto:
cargo new my_math_lib
cd my_math_libEdite o arquivo src/lib.rs para incluir suas implementações e testes.
Tarefas:
-
1. Função
is_even:- Crie uma função
pub fn is_even(num: i32) -> boolque retornetruese o número for par efalsecaso contrário. - Escreva pelo menos 3 testes unitários para esta função:
- Um para um número par.
- Um para um número ímpar.
- Um para o número zero.
- Crie uma função
-
2. Função
factorial:- Crie uma função
pub fn factorial(n: u32) -> u128que calcule o fatorial de um númeron. - Escreva pelo menos 4 testes unitários para esta função:
factorial(0)deve ser 1.factorial(1)deve ser 1.factorial(5)deve ser 120.- Um teste com
#[should_panic]parafactorialse você decidir que números negativos devem causar pânico (embora a assinaturau32já previna isso, você pode adaptar parai32para este teste, ou criar uma condição de pânico para umu32muito grande que causaria overflow se você não usasseu128). Dica: parau32, você pode simular um pânico se o número for excessivamente grande para a capacidade de cálculo, ou simplesmente testar os casos normais.
- Crie uma função
-
3. Função
dividecomResult:- Crie uma função
pub fn divide(numerator: f64, denominator: f64) -> Result<f64, String>que divida dois números. - Se o denominador for zero, a função deve retornar um
Errcom uma mensagem apropriada (ex: "Cannot divide by zero"). - Escreva pelo menos 3 testes unitários para esta função:
- Um para uma divisão bem-sucedida (ex:
divide(10.0, 2.0)). - Um para o caso de divisão por zero que retorne
Err. - Um teste que use o operador
?no teste para uma divisão bem-sucedida.
- Um para uma divisão bem-sucedida (ex:
- Crie uma função
-
4. Experimente com
cargo test:- Execute todos os seus testes.
- Execute apenas os testes da função
factorial. - Execute os testes com a opção
-- --nocapturee adicione umprintln!dentro de um dos seus testes para ver a saída.
Sinta-se à vontade para adicionar mais testes e explorar outros cenários!
4. Resumo e Próximos Passos 🚀
Parabéns! 🎉 Você deu os primeiros passos cruciais no mundo dos testes unitários em Rust. Nesta aula, você aprendeu:
- A importância dos testes unitários para a qualidade do código.
- Como estruturar seus testes usando
#[cfg(test)]emod tests. - A utilizar as macros
assert!,assert_eq!eassert_ne!para verificar o comportamento do código. - A testar condições de erro e pânico com
#[should_panic]. - A lidar com testes que retornam
Result<T, E>. - A usar
cargo teste suas opções para gerenciar a execução dos testes.
Dominar os testes unitários é fundamental para escrever código Rust robusto e confiável. Eles são a primeira linha de defesa contra bugs e uma ferramenta poderosa para a refatoração.
Próximos Passos:
- Testes de Integração: No próximo tópico, exploraremos como escrever testes de integração, que testam a interação entre diferentes partes do seu crate ou com crates externas.
- Testes de Documentação: Rust também permite escrever testes diretamente nos exemplos da sua documentação, garantindo que a documentação esteja sempre atualizada e correta.
- Testes de Benchmark: Para medir o desempenho do seu código.
Continue praticando e explorando! A capacidade de testar seu código eficientemente o tornará um desenvolvedor Rust muito mais produtivo e confiante. Até a próxima! 👋