Rust tem suporte nativo a testes — sem frameworks externos, sem configuração adicional. O compilador e o Cargo sabem o que são testes, como executá-los e como reportar resultados. Essa integração não é acidental: a cultura Rust valoriza profundamente a correção do software, e testes são parte central dessa cultura.
Neste artigo vamos cobrir testes unitários, testes de integração, organização de testes em projetos reais, e algumas técnicas avançadas que tornam seus testes mais expressivos.
Testes unitários — a forma mais simples
Um teste em Rust é qualquer função anotada com #[test]. O Cargo os encontra e executa automaticamente:
fn somar(a: i32, b: i32) -> i32 {
a + b
}
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
#[cfg(test)]
mod tests {
use super::*; // importa tudo do módulo pai
#[test]
fn teste_somar_positivos() {
assert_eq!(somar(2, 3), 5);
}
#[test]
fn teste_somar_negativos() {
assert_eq!(somar(-2, -3), -5);
}
#[test]
fn teste_somar_zero() {
assert_eq!(somar(0, 5), 5);
assert_eq!(somar(5, 0), 5);
}
#[test]
fn teste_dividir_normal() {
assert_eq!(dividir(10.0, 2.0), Some(5.0));
}
#[test]
fn teste_dividir_por_zero() {
assert_eq!(dividir(10.0, 0.0), None);
}
}
Execute com:
cargo test
Saída:
running 5 tests
test tests::teste_somar_negativos ... ok
test tests::teste_somar_positivos ... ok
test tests::teste_somar_zero ... ok
test tests::teste_dividir_normal ... ok
test tests::teste_dividir_por_zero ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured
O bloco #[cfg(test)] instrui o compilador a incluir esse módulo apenas em compilações de teste — não vai para o binário de produção. use super::* importa tudo do módulo pai para que o código de teste tenha acesso às funções que testa.
As macros de asserção
Rust oferece várias macros de asserção com mensagens de erro informativas:
#[cfg(test)]
mod tests {
#[test]
fn demonstrar_assercoes() {
// assert! — verifica condição booleana
assert!(2 + 2 == 4);
assert!(!"hello".is_empty());
// assert_eq! — verifica igualdade
assert_eq!(2 + 2, 4);
assert_eq!("hello".to_uppercase(), "HELLO");
// assert_ne! — verifica desigualdade
assert_ne!(2 + 2, 5);
assert_ne!("hello", "world");
// Mensagens customizadas de falha
let x = 42;
assert_eq!(x, 42, "esperava 42, mas x era {x}");
assert!(
x > 0,
"x deveria ser positivo, mas era {}",
x
);
}
#[test]
fn comparar_floats() {
let resultado = 0.1 + 0.2;
// Floats nunca devem ser comparados com ==
// Use uma margem de tolerância
let tolerancia = 1e-10;
assert!(
(resultado - 0.3).abs() < tolerancia,
"Esperava ~0.3, mas obteve {resultado}"
);
}
}
Quando um assert_eq! falha, Rust exibe ambos os valores automaticamente:
thread 'tests::meu_teste' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `6`'
Testes que devem entrar em pânico
Às vezes você quer verificar que uma função entra em pânico sob certas condições. Use #[should_panic]:
fn dividir_inteiro(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divisão por zero!");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Divisão por zero")]
fn teste_panic_divisao() {
dividir_inteiro(10, 0);
}
#[test]
fn teste_divisao_normal() {
assert_eq!(dividir_inteiro(10, 2), 5);
}
}
O atributo expected verifica que a mensagem do panic contém a string especificada. Sem expected, qualquer panic faz o teste passar — o que pode mascarar panics inesperados.
Testes com Result
Testes podem retornar Result<(), E>, permitindo o uso do operador ?:
use std::num::ParseIntError;
fn parse_e_dobrar(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.trim().parse()?;
Ok(n * 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn teste_parse_valido() -> Result<(), ParseIntError> {
let resultado = parse_e_dobrar("21")?;
assert_eq!(resultado, 42);
Ok(())
}
#[test]
fn teste_parse_invalido() {
let resultado = parse_e_dobrar("abc");
assert!(resultado.is_err());
}
}
Quando um teste retorna Err, ele falha com a mensagem de erro — sem precisar de unwrap ou assert.
Controlando a execução dos testes
O Cargo oferece vários modificadores para controlar como os testes rodam:
# Rodar apenas testes com "somar" no nome
cargo test somar
# Rodar todos os testes, incluindo ignorados
cargo test -- --include-ignored
# Rodar testes sequencialmente (sem paralelismo)
cargo test -- --test-threads=1
# Mostrar output de println! nos testes
cargo test -- --nocapture
# Listar todos os testes sem executar
cargo test -- --list
Por padrão, println! dentro de testes é suprimido — só aparece quando o teste falha. Use --nocapture para ver a saída sempre.
Ignorando testes temporariamente
Use #[ignore] para marcar testes lentos ou não implementados:
#[cfg(test)]
mod tests {
#[test]
fn teste_rapido() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore = "teste muito lento — roda apenas em CI"]
fn teste_lento() {
// Simula operação demorada
std::thread::sleep(std::time::Duration::from_secs(5));
assert!(true);
}
#[test]
#[ignore = "ainda não implementado"]
fn teste_futuro() {
todo!("implementar quando a feature X estiver pronta")
}
}
Testes de integração
Testes unitários verificam partes isoladas do código. Testes de integração verificam que as partes funcionam juntas, usando o crate como um cliente externo usaria.
Eles vivem no diretório tests/ na raiz do projeto:
meu_projeto/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ └── calculadora.rs
└── tests/
├── integracao_calculadora.rs
└── integracao_relatorio.rs
src/lib.rs:
pub mod calculadora;
src/calculadora.rs:
pub fn somar(a: f64, b: f64) -> f64 { a + b }
pub fn subtrair(a: f64, b: f64) -> f64 { a - b }
pub fn multiplicar(a: f64, b: f64) -> f64 { a * b }
pub fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
pub fn media(valores: &[f64]) -> Option<f64> {
if valores.is_empty() {
return None;
}
Some(valores.iter().sum::<f64>() / valores.len() as f64)
}
tests/integracao_calculadora.rs:
// Testes de integração usam o crate como dependência externa
use meu_projeto::calculadora;
#[test]
fn operacoes_basicas_encadeadas() {
let a = calculadora::somar(10.0, 5.0); // 15
let b = calculadora::multiplicar(a, 2.0); // 30
let c = calculadora::subtrair(b, 6.0); // 24
let resultado = calculadora::dividir(c, 4.0); // Some(6)
assert_eq!(resultado, Some(6.0));
}
#[test]
fn media_de_resultados() {
let valores = vec![
calculadora::somar(1.0, 2.0), // 3
calculadora::multiplicar(2.0, 3.0), // 6
calculadora::subtrair(10.0, 1.0), // 9
];
let media = calculadora::media(&valores);
assert_eq!(media, Some(6.0));
}
#[test]
fn divisao_por_zero_retorna_none() {
assert_eq!(calculadora::dividir(100.0, 0.0), None);
}
#[test]
fn media_de_lista_vazia() {
let lista: Vec<f64> = vec![];
assert_eq!(calculadora::media(&lista), None);
}
Organizando testes com módulos auxiliares
Em testes de integração, código compartilhado entre arquivos vai em tests/common/mod.rs:
tests/
├── common/
│ └── mod.rs ← utilitários compartilhados
├── integracao_calculadora.rs
└── integracao_relatorio.rs
tests/common/mod.rs:
// Helpers compartilhados entre testes de integração
pub fn aproximadamente_igual(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-10
}
pub fn criar_lista_teste() -> Vec<f64> {
vec![1.0, 2.0, 3.0, 4.0, 5.0]
}
#[allow(dead_code)]
pub struct Contexto {
pub nome: String,
pub valores: Vec<f64>,
}
impl Contexto {
pub fn novo(nome: &str) -> Self {
Contexto {
nome: nome.to_string(),
valores: criar_lista_teste(),
}
}
}
Usando em testes:
mod common;
use meu_projeto::calculadora;
#[test]
fn teste_com_helper() {
let ctx = common::Contexto::novo("teste_media");
let media = calculadora::media(&ctx.valores).unwrap();
assert!(common::aproximadamente_igual(media, 3.0));
}
Um projeto completo com testes abrangentes
Vamos criar um módulo de validação com cobertura completa de testes:
src/validacao.rs:
#[derive(Debug, PartialEq)]
pub enum ErroValidacao {
CampoVazio(String),
TamanhoInvalido { campo: String, min: usize, max: usize },
FormatoInvalido(String),
ValorForaDoIntervalo { campo: String, min: f64, max: f64 },
}
impl std::fmt::Display for ErroValidacao {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ErroValidacao::CampoVazio(c) =>
write!(f, "Campo '{c}' não pode ser vazio"),
ErroValidacao::TamanhoInvalido { campo, min, max } =>
write!(f, "Campo '{campo}' deve ter entre {min} e {max} caracteres"),
ErroValidacao::FormatoInvalido(msg) =>
write!(f, "Formato inválido: {msg}"),
ErroValidacao::ValorForaDoIntervalo { campo, min, max } =>
write!(f, "Campo '{campo}' deve estar entre {min} e {max}"),
}
}
}
pub fn validar_nome(nome: &str) -> Result<&str, ErroValidacao> {
let nome = nome.trim();
if nome.is_empty() {
return Err(ErroValidacao::CampoVazio("nome".to_string()));
}
if nome.len() < 2 || nome.len() > 100 {
return Err(ErroValidacao::TamanhoInvalido {
campo: "nome".to_string(),
min: 2,
max: 100,
});
}
if !nome.chars().all(|c| c.is_alphabetic() || c == ' ') {
return Err(ErroValidacao::FormatoInvalido(
"nome deve conter apenas letras e espaços".to_string()
));
}
Ok(nome)
}
pub fn validar_email(email: &str) -> Result<&str, ErroValidacao> {
let email = email.trim();
if email.is_empty() {
return Err(ErroValidacao::CampoVazio("email".to_string()));
}
let partes: Vec<&str> = email.split('@').collect();
if partes.len() != 2 || partes[0].is_empty() || partes[1].is_empty() {
return Err(ErroValidacao::FormatoInvalido(
"email deve conter exatamente um '@'".to_string()
));
}
if !partes[1].contains('.') {
return Err(ErroValidacao::FormatoInvalido(
"domínio do email deve conter '.'".to_string()
));
}
Ok(email)
}
pub fn validar_idade(idade: f64) -> Result<f64, ErroValidacao> {
if idade < 0.0 || idade > 150.0 {
return Err(ErroValidacao::ValorForaDoIntervalo {
campo: "idade".to_string(),
min: 0.0,
max: 150.0,
});
}
Ok(idade)
}
#[cfg(test)]
mod tests {
use super::*;
// ── Testes de validar_nome ─────────────────────
#[test]
fn nome_valido_simples() {
assert_eq!(validar_nome("Ana"), Ok("Ana"));
}
#[test]
fn nome_valido_com_espaco() {
assert_eq!(validar_nome("Ana Silva"), Ok("Ana Silva"));
}
#[test]
fn nome_valido_com_espacos_extras() {
// trim deve remover espaços
assert_eq!(validar_nome(" Ana "), Ok("Ana"));
}
#[test]
fn nome_vazio_retorna_erro() {
assert_eq!(
validar_nome(""),
Err(ErroValidacao::CampoVazio("nome".to_string()))
);
}
#[test]
fn nome_apenas_espacos_retorna_erro() {
assert_eq!(
validar_nome(" "),
Err(ErroValidacao::CampoVazio("nome".to_string()))
);
}
#[test]
fn nome_muito_curto_retorna_erro() {
assert_eq!(
validar_nome("A"),
Err(ErroValidacao::TamanhoInvalido {
campo: "nome".to_string(),
min: 2,
max: 100,
})
);
}
#[test]
fn nome_com_numero_retorna_erro() {
assert!(matches!(
validar_nome("Ana2"),
Err(ErroValidacao::FormatoInvalido(_))
));
}
// ── Testes de validar_email ────────────────────
#[test]
fn email_valido() {
assert_eq!(
validar_email("ana@exemplo.com"),
Ok("ana@exemplo.com")
);
}
#[test]
fn email_vazio_retorna_erro() {
assert!(matches!(
validar_email(""),
Err(ErroValidacao::CampoVazio(_))
));
}
#[test]
fn email_sem_arroba_retorna_erro() {
assert!(matches!(
validar_email("anasemdominio"),
Err(ErroValidacao::FormatoInvalido(_))
));
}
#[test]
fn email_sem_dominio_retorna_erro() {
assert!(matches!(
validar_email("ana@"),
Err(ErroValidacao::FormatoInvalido(_))
));
}
#[test]
fn email_dominio_sem_ponto_retorna_erro() {
assert!(matches!(
validar_email("ana@dominio"),
Err(ErroValidacao::FormatoInvalido(_))
));
}
// ── Testes de validar_idade ────────────────────
#[test]
fn idade_valida() {
assert_eq!(validar_idade(25.0), Ok(25.0));
}
#[test]
fn idade_zero_valida() {
assert_eq!(validar_idade(0.0), Ok(0.0));
}
#[test]
fn idade_maxima_valida() {
assert_eq!(validar_idade(150.0), Ok(150.0));
}
#[test]
fn idade_negativa_retorna_erro() {
assert!(matches!(
validar_idade(-1.0),
Err(ErroValidacao::ValorForaDoIntervalo { .. })
));
}
#[test]
fn idade_acima_maximo_retorna_erro() {
assert!(matches!(
validar_idade(151.0),
Err(ErroValidacao::ValorForaDoIntervalo { .. })
));
}
// ── Teste de mensagens de erro ─────────────────
#[test]
fn mensagem_campo_vazio() {
let erro = ErroValidacao::CampoVazio("nome".to_string());
assert_eq!(erro.to_string(), "Campo 'nome' não pode ser vazio");
}
#[test]
fn mensagem_tamanho_invalido() {
let erro = ErroValidacao::TamanhoInvalido {
campo: "senha".to_string(),
min: 8,
max: 50,
};
assert_eq!(
erro.to_string(),
"Campo 'senha' deve ter entre 8 e 50 caracteres"
);
}
}
Execute com cargo test e veja todos os 18 testes passando:
running 18 tests
test tests::nome_valido_simples ... ok
test tests::nome_valido_com_espaco ... ok
test tests::nome_valido_com_espacos_extras ... ok
test tests::nome_vazio_retorna_erro ... ok
test tests::nome_apenas_espacos_retorna_erro ... ok
test tests::nome_muito_curto_retorna_erro ... ok
test tests::nome_com_numero_retorna_erro ... ok
test tests::email_valido ... ok
test tests::email_vazio_retorna_erro ... ok
test tests::email_sem_arroba_retorna_erro ... ok
test tests::email_sem_dominio_retorna_erro ... ok
test tests::email_dominio_sem_ponto_retorna_erro ... ok
test tests::idade_valida ... ok
test tests::idade_zero_valida ... ok
test tests::idade_maxima_valida ... ok
test tests::idade_negativa_retorna_erro ... ok
test tests::idade_acima_maximo_retorna_erro ... ok
test tests::mensagem_campo_vazio ... ok
test tests::mensagem_tamanho_invalido ... ok
test result: ok. 19 passed; 0 failed
Boas práticas de testes em Rust
Com o tempo, alguns padrões se mostram consistentemente úteis:
Nomeie testes como especificações. Um bom nome de teste lê como uma frase: nome_vazio_retorna_erro, divisao_por_zero_retorna_none. Quando o teste falha, o nome já diz o que aconteceu.
Teste os casos de borda. Valores zero, listas vazias, strings vazias, valores no limite do intervalo — esses são os casos onde bugs se escondem. Cubra-os explicitamente.
Um conceito por teste. Cada teste deve verificar uma única coisa. Se um teste falha, você sabe exatamente o que quebrou. Testes que verificam muitas coisas ao mesmo tempo são difíceis de diagnosticar.
Use matches! para verificar variantes sem comparar dados. Quando você quer saber se um Result é Err de um tipo específico, mas não se importa com o valor exato do erro, matches! é mais limpo que um match completo.
Teste o comportamento, não a implementação. Bons testes verificam o que a função faz, não como ela faz. Isso permite refatorar a implementação sem precisar reescrever os testes.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 11 — Writing Automated Tests — https://doc.rust-lang.org/book/ch11-00-testing.html
- Rust by Example — Testing — https://doc.rust-lang.org/rust-by-example/testing.html
- The Cargo Book — Tests — https://doc.rust-lang.org/cargo/commands/cargo-test.html
- "Arrange, Act, Assert" — padrão clássico de estruturação de testes — https://wiki.c2.com/?ArrangeActAssert
proptestcrate — property-based testing em Rust — https://docs.rs/proptestmockallcrate — mocking em Rust — https://docs.rs/mockall- Rustlings, seção
tests— https://github.com/rust-lang/rustlings