Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Como escrever testes

Testes são funções Rust que verificam se o código que não é de teste está se comportando da forma esperada. Os corpos das funções de teste normalmente executam estas três ações:

  • Configurar os dados ou o estado necessários.
  • Executar o código que você quer testar.
  • Verificar se os resultados são os esperados.

Vamos ver os recursos que Rust fornece especificamente para escrever testes que sigam esse fluxo, incluindo o atributo test, algumas macros e o atributo should_panic.

Estruturando funções de teste

Na forma mais simples, um teste em Rust é uma função anotada com o atributo test. Atributos são metadados sobre partes do código Rust; um exemplo é o atributo derive, que usamos com structs no Capítulo 5. Para transformar uma função em uma função de teste, adicione #[test] na linha anterior a fn. Quando você executa seus testes com o comando cargo test, Rust constrói um binário executor de testes que executa as funções anotadas e informa se cada função de teste passou ou falhou.

Sempre que criamos um novo projeto de biblioteca com Cargo, um módulo de testes com uma função de teste dentro dele é gerado automaticamente. Esse módulo oferece um modelo para escrever seus testes, para que você não precise relembrar a estrutura e a sintaxe exatas toda vez que iniciar um projeto novo. Você pode adicionar quantas funções de teste extras e quantos módulos de teste quiser!

Vamos explorar alguns aspectos de como os testes funcionam experimentando o modelo inicial antes de realmente testar qualquer código. Depois, escreveremos alguns testes mais realistas que chamam código escrito por nós e verificam se seu comportamento está correto.

Vamos criar um novo projeto de biblioteca chamado adder, que somará dois números:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

O conteúdo do arquivo src/lib.rs na sua biblioteca adder deve se parecer com a Listagem 11-1.

Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-1: Código gerado automaticamente por cargo new

O arquivo começa com uma função de exemplo, add, para que tenhamos algo a testar.

Por enquanto, vamos focar apenas na função it_works. Observe a anotação #[test]: esse atributo indica que esta é uma função de teste, para que o executor saiba tratá-la como tal. Também podemos ter funções que não são testes dentro do módulo tests para ajudar a montar cenários comuns ou executar operações repetidas, por isso sempre precisamos indicar quais funções são testes.

O corpo da função de exemplo usa a macro assert_eq! para verificar que result, que contém o resultado de chamar add com 2 e 2, é igual a 4. Essa verificação serve como exemplo do formato de um teste típico. Vamos executá-lo para confirmar que esse teste passa.

O comando cargo test executa todos os testes do nosso projeto, como mostra a Listagem 11-2.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Listing 11-2: Saída da execução do teste gerado automaticamente

Cargo compilou e executou o teste. Vemos a linha running 1 test. A linha seguinte mostra o nome da função de teste gerada, tests::it_works, e que o resultado da execução desse teste é ok. O resumo geral test result: ok. significa que todos os testes passaram, e a parte que diz 1 passed; 0 failed totaliza quantos testes passaram ou falharam.

É possível marcar um teste como ignorado para que ele não seja executado em determinada ocasião; veremos isso mais adiante neste capítulo, na seção “Ignorando testes, a menos que sejam solicitados especificamente”

. Como ainda não fizemos isso aqui, o resumo mostra

0 ignored. Também podemos passar um argumento ao comando cargo test para executar somente os testes cujo nome corresponda a uma string; isso é chamado de filtragem, e veremos esse recurso na seção “Executando um subconjunto de testes por nome”. Aqui, não filtramos os testes executados, então o fim do resumo mostra 0 filtered out.

A estatística 0 measured é usada para testes de benchmark que medem desempenho. No momento em que este livro foi escrito, testes de benchmark só estavam disponíveis no Rust noturno. Veja a documentação sobre testes de benchmark para saber mais.

A próxima parte da saída, que começa em Doc-tests adder, corresponde aos resultados de quaisquer testes de documentação. Ainda não temos testes de documentação, mas Rust pode compilar qualquer exemplo de código que apareça na nossa documentação de API. Esse recurso ajuda a manter documentação e código em sincronia! Vamos discutir como escrever testes de documentação na seção “Comentários de documentação como testes” do Capítulo 14. Por enquanto, vamos ignorar a saída Doc-tests.

Vamos começar a adaptar o teste às nossas necessidades. Primeiro, altere o nome da função it_works para outro nome, como exploration, assim:

Nome do arquivo: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Depois, execute cargo test novamente. Agora a saída mostra exploration em vez de it_works:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::exploration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Agora vamos adicionar outro teste, mas desta vez faremos um teste que falha! Testes falham quando algo dentro da função de teste entra em pânico. Cada teste é executado em uma nova thread, e quando a thread principal percebe que uma thread de teste morreu, esse teste é marcado como falho. No Capítulo 9, comentamos que a forma mais simples de provocar um pânico é chamar a macro panic!. Digite o novo teste como uma função chamada another, para que seu arquivo src/lib.rs fique como na Listagem 11-3.

Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn exploration() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}
Listing 11-3: Adicionando um segundo teste que falhará porque chamamos a macro panic!

Execute os testes novamente com cargo test. A saída deve se parecer com a Listagem 11-4, que mostra que nosso teste exploration passou e another falhou.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----

thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`
Listing 11-4: Resultados dos testes quando um passa e outro falha

Em vez de ok, a linha test tests::another mostra FAILED. Duas novas seções aparecem entre os resultados individuais e o resumo. A primeira exibe o motivo detalhado de cada falha de teste. Neste caso, vemos que tests::another falhou porque entrou em pânico com a mensagem Make this test fail na linha 17 do arquivo src/lib.rs. A seção seguinte lista apenas os nomes de todos os testes que falharam, o que é útil quando há muitos testes e muitas saídas detalhadas de falha. Podemos usar o nome de um teste que falhou para executá-lo isoladamente e depurá-lo com mais facilidade; falaremos mais sobre formas de executar testes na seção “Controlando como os testes são executados”

.

A linha de resumo aparece ao final: no geral, o resultado do nosso teste é FAILED. Tivemos um teste que passou e um teste que falhou.

Agora que você já viu como os resultados de teste se apresentam em cenários diferentes, vamos examinar algumas macros além de panic! que são úteis em testes.

Verificando resultados com assert!

A macro assert!, fornecida pela biblioteca padrão, é útil quando você quer garantir que alguma condição em um teste seja avaliada como true. Passamos à macro assert! um argumento que é avaliado como um booleano. Se o valor for true, nada acontece e o teste passa. Se o valor for false, a macro assert! chama panic! para fazer o teste falhar. Usar a macro assert! nos ajuda a verificar se o código está funcionando da maneira que pretendemos.

No Capítulo 5, na Listagem 5-15, usamos uma struct Rectangle e um método can_hold, repetidos aqui na Listagem 11-5. Vamos colocar esse código no arquivo src/lib.rs e, em seguida, escrever alguns testes para ele usando a macro assert!.

Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}
Listing 11-5: A struct Rectangle e seu método can_hold, do Capítulo 5

O método can_hold retorna um booleano, o que o torna um caso de uso perfeito para a macro assert!. Na Listagem 11-6, escrevemos um teste que exercita o método can_hold criando uma instância de Rectangle com largura 8 e altura 7 e verificando que ela consegue conter outra instância de Rectangle com largura 5 e altura 1.

Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
}
Listing 11-6: Um teste para can_hold que verifica se um retângulo maior realmente consegue conter um retângulo menor

Observe a linha use super::*; dentro do módulo tests. O módulo tests é um módulo comum que segue as regras usuais de visibilidade que vimos no Capítulo 7 na seção “Caminhos para se referir a um item na árvore de módulos”

. Como o módulo `tests` é um módulo interno, precisamos trazer

para o escopo do módulo interno o código em teste definido no módulo externo. Usamos um glob aqui, então tudo o que definirmos no módulo externo fica disponível para esse módulo tests.

Chamamos nosso teste de larger_can_hold_smaller e criamos as duas instâncias de Rectangle de que precisamos. Em seguida, chamamos a macro assert! e passamos o resultado da chamada larger.can_hold(&smaller). Essa expressão deve retornar true, então nosso teste deve passar. Vamos conferir!

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 1 test
test tests::larger_can_hold_smaller ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Ele passa! Vamos adicionar outro teste, desta vez verificando que um retângulo menor não pode conter um retângulo maior:

Nome do arquivo: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        // --snip--
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Como o resultado correto da função can_hold nesse caso é false, precisamos negar esse resultado antes de passá-lo à macro assert!. Como resultado, nosso teste vai passar se can_hold retornar false:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Dois testes passando! Agora vamos ver o que acontece com os resultados quando introduzimos um bug no nosso código. Vamos mudar a implementação do método can_hold, substituindo o sinal de maior que (>) por um sinal de menor que (<) na comparação das larguras:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

// --snip--
impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

Executando os testes agora, obtemos o seguinte:

$ cargo test
   Compiling rectangle v0.1.0 (file:///projects/rectangle)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok

failures:

---- tests::larger_can_hold_smaller stdout ----

thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Nossos testes detectaram o bug! Como larger.width é 8 e smaller.width é 5, a comparação das larguras em can_hold agora retorna false: 8 não é menor que 5.

Testando igualdade com assert_eq! e assert_ne!

Uma forma comum de verificar funcionalidade é testar a igualdade entre o resultado do código em teste e o valor que você espera que esse código retorne. Você poderia fazer isso usando a macro assert! e passando a ela uma expressão com o operador ==. Porém, isso é tão comum em testes que a biblioteca padrão fornece um par de macros, assert_eq! e assert_ne!, para realizar essa verificação de forma mais conveniente. Essas macros comparam dois argumentos quanto à igualdade ou à desigualdade, respectivamente. Elas também imprimem os dois valores quando a verificação falha, o que facilita entender por que o teste falhou; em contraste, a macro assert! apenas indica que recebeu false para a expressão com ==, sem mostrar os valores que levaram a esse resultado.

Na Listagem 11-7, escrevemos uma função chamada add_two, que soma 2 ao seu parâmetro, e depois testamos essa função usando a macro assert_eq!.

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    a + 2
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}
Listing 11-7: Testando a função add_two com a macro assert_eq!

Vamos verificar se ela passa!

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Criamos uma variável chamada result que guarda o resultado de chamar add_two(2). Em seguida, passamos result e 4 como argumentos para a macro assert_eq!. A linha de saída desse teste é test tests::it_adds_two ... ok, e o texto ok indica que nosso teste passou!

Vamos introduzir um bug no código para ver como assert_eq! se comporta quando falha. Altere a implementação da função add_two para que ela some 3:

pub fn add_two(a: u64) -> u64 {
    a + 3
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds_two() {
        let result = add_two(2);
        assert_eq!(result, 4);
    }
}

Execute os testes novamente:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_adds_two ... FAILED

failures:

---- tests::it_adds_two stdout ----

thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
  left: 5
 right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Nosso teste detectou o bug! O teste tests::it_adds_two falhou, e a mensagem nos informa que a verificação que falhou foi left == right e quais são os valores left e right. Essa mensagem já nos ajuda a começar a depuração: o argumento left, no qual tínhamos o resultado da chamada add_two(2), era 5, enquanto o argumento right era 4. Você pode imaginar como isso é especialmente útil quando temos muitos testes em execução.

Observe que, em algumas linguagens e frameworks de teste, os parâmetros das funções de verificação de igualdade são chamados de expected e actual, e a ordem em que especificamos os argumentos importa. Em Rust, porém, eles se chamam left e right, e a ordem em que fornecemos o valor esperado e o valor produzido pelo código não importa. Poderíamos escrever a verificação desse teste como assert_eq!(4, result), o que produziria a mesma mensagem de falha que mostra assertion `left == right` failed.

A macro assert_ne! passa se os dois valores fornecidos forem diferentes e falha se forem iguais. Ela é mais útil em casos em que não sabemos exatamente qual valor será produzido, mas sabemos qual valor definitivamente não deve ser. Por exemplo, se estivermos testando uma função que certamente modifica sua entrada de alguma maneira, mas a forma dessa modificação depende do dia da semana em que executamos os testes, talvez a melhor verificação seja afirmar que a saída da função não é igual à entrada.

Por baixo dos panos, as macros assert_eq! e assert_ne! usam os operadores == e !=, respectivamente. Quando as verificações falham, essas macros imprimem seus argumentos usando formatação de depuração, o que significa que os valores comparados precisam implementar as traits PartialEq e Debug. Todos os tipos primitivos e a maior parte dos tipos da biblioteca padrão implementam essas traits. Para structs e enums definidos por você, será necessário implementar PartialEq para verificar igualdade entre esses tipos. Também será necessário implementar Debug para imprimir os valores quando a verificação falhar. Como ambas são traits deriváveis, como mencionado na Listagem 5-12 do Capítulo 5, isso normalmente é tão simples quanto adicionar a anotação #[derive(PartialEq, Debug)] à definição da sua struct ou enum. Veja o Apêndice C, “Traits deriváveis”, para mais detalhes sobre essas e outras traits deriváveis.

Adicionando mensagens de falha personalizadas

Você também pode adicionar uma mensagem personalizada para ser impressa junto com a mensagem de falha como argumentos opcionais das macros assert!, assert_eq! e assert_ne!. Todos os argumentos especificados depois dos argumentos obrigatórios são repassados à macro format! (discutida em “Concatenação com + ou format!, no Capítulo 8), então você pode fornecer uma string de formatação com espaços reservados {} e os valores que devem preenchê-los. Mensagens personalizadas são úteis para documentar o que a verificação quer dizer; quando um teste falha, você terá uma noção melhor do que está errado no código.

Por exemplo, digamos que temos uma função que cumprimenta as pessoas pelo nome e queremos testar se o nome passado para a função aparece na saída:

Nome do arquivo: src/lib.rs

pub fn greeting(name: &str) -> String {
    format!("Hello {name}!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Os requisitos desse programa ainda não foram definidos por completo, e temos quase certeza de que o texto Hello no começo da saudação vai mudar. Decidimos que não queremos ter de atualizar o teste sempre que os requisitos mudarem; por isso, em vez de verificar igualdade exata com o valor retornado por greeting, vamos apenas verificar se a saída contém o texto do parâmetro de entrada.

Agora vamos introduzir um bug nesse código, alterando greeting para excluir name, para ver como é a mensagem de falha padrão:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(result.contains("Carol"));
    }
}

Executar esse teste produz o seguinte:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Esse resultado apenas indica que a verificação falhou e em qual linha ela está. Uma mensagem de falha mais útil mostraria o valor retornado por greeting. Vamos adicionar uma mensagem de falha personalizada, composta por uma string de formatação com um espaço reservado preenchido pelo valor real que obtivemos da função greeting:

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{result}`"
        );
    }
}

Agora, quando executarmos o teste, teremos uma mensagem de erro mais informativa:

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----

thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Conseguimos ver o valor que realmente obtivemos na saída do teste, o que ajuda a depurar o que aconteceu, em vez de apenas o que esperávamos que tivesse acontecido.

Verificando pânicos com should_panic

Além de verificar valores de retorno, é importante confirmar se o código lida com condições de erro da maneira esperada. Por exemplo, considere o tipo Guess que criamos no Capítulo 9, na Listagem 9-13. Outro código que usa Guess depende da garantia de que instâncias de Guess conterão apenas valores entre 1 e 100. Podemos escrever um teste que garanta que tentar criar uma instância de Guess com um valor fora desse intervalo provoque um pânico.

Fazemos isso adicionando o atributo should_panic à função de teste. O teste passa se o código dentro da função entrar em pânico; o teste falha se o código dentro da função não entrar em pânico.

A Listagem 11-8 mostra um teste que verifica se as condições de erro de Guess::new acontecem quando esperamos.

Filename: src/lib.rs
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::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}
Listing 11-8: Testando que uma condição provocará um panic!

Colocamos o atributo #[should_panic] depois do atributo #[test] e antes da função de teste à qual ele se aplica. Vamos ver o resultado quando esse teste passa:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests guessing_game

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Parece bom! Agora vamos introduzir um bug em nosso código removendo a condição de que a função new deve entrar em pânico quando o valor for maior que 100:

pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Quando executarmos o teste da Listagem 11-8, ele falhará:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Nesse caso, não recebemos uma mensagem muito útil, mas, olhando a função de teste, vemos que ela está anotada com #[should_panic]. A falha que obtivemos significa que o código dentro da função de teste não provocou pânico.

Testes que usam should_panic podem ser imprecisos. Um teste com should_panic passa mesmo que o código entre em pânico por um motivo diferente daquele que esperávamos. Para tornar esses testes mais precisos, podemos adicionar um parâmetro opcional expected ao atributo should_panic. O executor de testes verificará se a mensagem de falha contém o texto fornecido. Por exemplo, considere o código modificado de Guess na Listagem 11-9, em que a função new entra em pânico com mensagens diferentes dependendo de o valor ser pequeno demais ou grande demais.

Filename: src/lib.rs
pub struct Guess {
    value: i32,
}

// --snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}
Listing 11-9: Testando um panic! com uma mensagem de pânico que contém uma substring especificada

Esse teste passará porque o valor que colocamos no parâmetro expected do atributo should_panic é uma substring da mensagem com a qual a função Guess::new entra em pânico. Poderíamos ter especificado a mensagem completa de pânico esperada, que nesse caso seria Guess value must be less than or equal to 100, got 200. O que você escolhe especificar depende de quanto da mensagem é único ou dinâmico e de quão preciso você quer que o teste seja. Neste caso, uma substring da mensagem de pânico já basta para garantir que o código da função de teste execute o caso else if value > 100.

Para ver o que acontece quando um teste com should_panic e mensagem expected falha, vamos introduzir novamente um bug no nosso código trocando os corpos dos blocos if value < 1 e else if value > 100:

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be less than or equal to 100, got {value}."
            );
        } else if value > 100 {
            panic!(
                "Guess value must be greater than or equal to 1, got {value}."
            );
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Desta vez, quando executarmos o teste should_panic, ele falhará:

$ cargo test
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)

running 1 test
test tests::greater_than_100 - should panic ... FAILED

failures:

---- tests::greater_than_100 stdout ----

thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: "Guess value must be greater than or equal to 1, got 200."
 expected substring: "less than or equal to 100"

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

A mensagem de falha indica que esse teste realmente entrou em pânico como esperávamos, mas a mensagem de pânico não continha a string esperada less than or equal to 100. A mensagem de pânico que recebemos neste caso foi Guess value must be greater than or equal to 1, got 200. Agora já podemos começar a descobrir onde está o bug!

Usando Result<T, E> em testes

Até agora, todos os nossos testes entram em pânico quando falham. Também podemos escrever testes que usem Result<T, E>! Aqui está o teste da Listagem 11-1, reescrito para usar Result<T, E> e retornar um Err em vez de entrar em pânico:

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() -> Result<(), String> {
        let result = add(2, 2);

        if result == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

A função it_works agora tem tipo de retorno Result<(), String>. No corpo da função, em vez de chamar a macro assert_eq!, retornamos Ok(()) quando o teste passa e um Err contendo uma String quando ele falha.

Escrever testes para que retornem um Result<T, E> permite usar o operador de interrogação no corpo dos testes, o que pode ser uma maneira conveniente de escrever testes que devem falhar se qualquer operação interna retornar uma variante Err.

Você não pode usar a anotação #[should_panic] em testes que usam Result<T, E>. Para verificar que uma operação retorna uma variante Err, não use o operador de interrogação sobre o valor Result<T, E>. Em vez disso, use assert!(value.is_err()).

Agora que você conhece várias maneiras de escrever testes, vamos ver o que acontece quando executamos nossos testes e explorar as diferentes opções que podemos usar com cargo test.