Pânico não é sentença: por que testar situações de emergência em Rust?
\n\nImagine: você está escrevendo em Rust uma biblioteca para trabalhar com dinheiro. A função divide_money(amount: u64, parts: u64) deve entrar em pânico se parts for igual a zero — afinal, não se pode dividir por zero, e você não quis retornar Option ou Result por razões arquiteturais. Como garantir que esse pânico realmente ocorre? Ou você está desenvolvendo um parser que deve falhar ao receber dados inválidos. Sem testes de pânico, você corre o risco de ter bugs "silenciosos" que só aparecerão em produção.
Em Rust, testar pânico não é apenas uma "opção agradável", mas uma ferramenta importante para criar código confiável. Neste artigo, vamos abordar como escrever testes unitários que verificam se seu código realmente entra em pânico nas situações necessárias. Você aprenderá sobre a macro #[should_panic], suas nuances, alternativas como std::panic::catch_unwind e melhores práticas. Vamos lá!
Abordagem clássica: macro #[should_panic]
\n\nA maneira mais simples e comum de verificar pânico em Rust é usar o atributo #[should_panic]. Ele é colocado sobre a função de teste e diz ao compilador: "Este teste é considerado bem-sucedido apenas se ocorrer um pânico internamente".
Exemplo básico
\n\n// lib.rs ou tests/unit_tests.rs\nfn divide(a: i32, b: i32) -> i32 {\n if b == 0 {\n panic!("Divisão por zero!");\n }\n a / b\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n #[should_panic]\n fn test_divide_by_zero() {\n divide(10, 0);\n }\n}\n\nExecute os testes com o comando cargo test — e você verá um resultado verde. Se o pânico não ocorrer (por exemplo, se você remover a verificação de zero), o teste falhará com a mensagem: "test test_divide_by_zero ... FAILED (expected to panic)".
Verificando a mensagem exata do pânico
\n\nÀs vezes, é necessário garantir não apenas o fato do pânico, mas sua mensagem específica. Isso ajuda a evitar falsos positivos, quando o pânico ocorre devido a outro erro. Use o parâmetro adicional expected:
#[test]\n#[should_panic(expected = "Divisão por zero!")]\nfn test_divide_by_zero_with_message() {\n divide(10, 0);\n}\n\n// Este teste não passará — a mensagem é diferente\n#[test]\n#[should_panic(expected = "Divisão por zero!")]\nfn test_other_panic() {\n panic!("Erro completamente diferente");\n}\n\nImportante: expected verifica se a mensagem de pânico contém a string especificada (não é uma igualdade exata). Isso é útil para mensagens longas.
Abordagem avançada: std::panic::catch_unwind
\n\nÀs vezes, #[should_panic] não é suficiente. Por exemplo, se você quiser verificar se o pânico ocorreu em um local específico, e não em outro, ou se precisar testar código assíncrono. Para esses casos, existe std::panic::catch_unwind — uma função que captura o pânico e o retorna como Result.
Como funciona
\n\nuse std::panic;\n\nfn risky_function(should_panic: bool) {\n if should_panic {\n panic!("Emergência!");\n }\n}\n\n#[test]\nfn test_catch_panic() {\n let result = panic::catch_unwind(|| {\n risky_function(true);\n });\n assert!(result.is_err()); // Verifica se houve pânico\n}\n\n#[test]\nfn test_no_panic() {\n let result = panic::catch_unwind(|| {\n risky_function(false);\n });\n assert!(result.is_ok()); // Não houve pânico\n}\n\nVerificando o tipo do pânico
\n\nCom catch_unwind, você pode até verificar se o pânico contém um valor específico (não apenas uma string):
use std::panic;\nuse std::any::Any;\n\nfn panic_with_number() {\n panic!(42); // Sim, é possível — pânico com um número!\n}\n\n#[test]\nfn test_panic_with_number() {\n let resu