Testes unitários para profissionais de dados com pytest — O Guia Definitivo Parte I

Economize tempo de desenvolvimento e manutenção, melhore a documentação do seu projeto e aumente a confiança do usuário final utilizando Testes Unitários

Vinícius Galvão
Data Hackers
7 min readFeb 28, 2021

--

Photo by JESHOOTS.COM on Unsplash

Suponha que você precisou implementar uma função para um script que você está desenvolvendo.

Como você normalmente testa se a função foi implementada corretamente?

O jeito mais “simples” seria chamar a função algumas vezes, passando diferentes argumentos.

Se der certo, você assume que a função foi implementada corretamente e vida que segue.

Embora esse método seja o mais simples e fácil de testar sua função, ele é muito ineficiente.

Isso fica bem evidente quando pensamos no ciclo de vida de uma função em um projeto de dados, que normalmente tem essa cara:

Nós implementamos a função, testamos, e caso passe no teste, nós implementamos.

Caso a função falhe, nós encontramos os erros que encontramos e testamos novamente.

Tempos depois, precisamos implementar uma nova feature ou refatorar a função, para então testarmos novamente.

Nesse momento, é bastante comum descobrir bugs não previstos antes. Com isso, corrigimos o bug e testamos novamente.

Consegue perceber a quantidade de vezes que a função precisou ser testada em seu ciclo de vida?

Se o projeto durar alguns anos, a função pode chegar a ser testada centenas de vezes.

Agora vamos estimar que cada vez que a função foi testada foram gastos, em média, 10 minutos. Se calcularmos a quantidade de horas destinadas a esses testes, nas 100 primeiras vezes, serão gastos um pouco mais de 16 horas (10 min x 100).

Photo by Aron Visuals on Unsplash

É nesse momento que vemos a importância dos testes unitários. Com eles, é possível automatizar o processo repetitivo de testes e, com isso, economizar bastante tempo.

Escrevendo seu primeiro teste unitário

Imagine que você implementou a função cast_to_int em preprocessing_helpers.py, que recebe um número em formato de string e converte para um inteiro.

Se quisermos testar essa função, podemos fazer da seguinte maneira:

Perceba que a função ainda retornou uma string, ou seja, ela não converteu a string para um inteiro.

Outro teste que podemos fazer é passar um inteiro para a função e analisar o que ela retornará.

Observe que a função retornou um erro, pois passamos um argumento que não é string para a função.

Poderíamos fazer vários outros testes manuais e analisar o que a função irá retornar, porém, como já discutimos, é muito mais produtivo criarmos testes unitários para essa função

Existem várias bibliotecas que nos ajudam a desenvolver testes unitários em Python.

  • pytest
  • unittest
  • doctest

Para esse tutorial, escolhi o pytest, por ser o mais popular e, na minha opinião, o mais fácil de utilizar.

Vamos criar um teste unitário para essa função em 5 passos:

1. Crie um arquivo de teste

Crie um arquivo chamado test_cast_to_int.py.

Existe uma convenção de que os arquivos começados com test_ indicam que há testes unitários neles.

2. Importe a função a ser testada

3. Crie a função de teste

Defina o argumento de teste que será passado para a função, o resultado esperado e o resultado obtido.

4. Assertion

Caso você não conheça como funciona o assert, por enquanto, basta entender que ele analisa uma expressão que retorna um boleano, caso a expressão seja verdadeira, ele não retorna nada, caso a expressão seja falsa, ele retorna um AssertionError. Por exemplo:

No nosso caso, queremos comparar se a função cast_to_int retornou o resultado que esperávamos. Então ficamos com:

Ou seja, esperamos que cast_to_int(“2.021”) seja igual à 2021.

5. Executar o teste unitário

Escreva isso no terminal:

O seguinte relatório é gerado:

Vamos entender o resultado por partes.

Primeiro, o relatório trás algumas informações gerais, indicando que os testes foram iniciados.

Em seguida, temos o resultado do teste.

O caractere F informa que o teste falhou e, com isso, devemos corrigir a função ou o teste unitário.

Seguindo adiante, temos informações dos testes que falharam.

Perceba que a linha que traz a exceção é marcada com um >.

Além disso, a exceção é uma AssertionError.

Por fim, temos um resumo dos resultados dos testes.

Vale ressaltar que o teste foi executado em 0.16 segundos, muito mais rápido se compararmos com os testes manuais.

Agora vamos analisar nossa função cast_to_int() e identificar o porquê que ela não está retornando o resultado esperado.

Observe que para que a função retorne um inteiro, precisamos colocar o resultado dentro de um int(), ficando dessa forma:

Agora, se executarmos novamente nosso teste unitário, obtemos o seguinte relatório:

Observe que agora no lugar de

Temos

O ponto verde indica que nosso teste passou!

Adicionando mensagens nos testes

Nós podemos ainda adicionar uma mensagem para os casos em que o resultado da expressão boleana for falso.

Por exemplo:

Com isso, podemos adicionar uma mensagem para o nosso teste unitário da seguinte maneira:

Caso a expressão actual == expected for Falsa, a mensagem será apresentada como uma AssertionError. Podendo ser bastante útil para identificar possíveis erros.

Testando exceções

No início do tutorial, nós testamos nossa função, cast_to_int(), passando um float como argumento.

Vimos que ela retorna um AttributeError, indicando que um float não possui o atributo replace.

Veremos agora como podemos criar testes unitários para esse tipo de situação.

Se escrevermos a função test_on_float():

Dessa maneira, caso a função retorne um AttributeError, o teste passa.

Podemos ainda testar se a mensagem apresentada no erro está de acordo com o que esperamos.

Nesse caso, expection_info armazena o AttributeError e exception_info.match(expected_msg) checa se expected_msg está presente no erro retornado.

Nosso arquivo test_cast_to_int.py agora possui essa cara:

Podemos executar novamente nosso teste unitário para analisarmos os resultados.

Perceba que os dois pontos verdes indicam que os dois testes passaram.

Múltiplos asserts em um único teste unitário

Nós podemos ainda utilizar mais de um assert em um teste unitário.

Por exemplo, se quisermos que o nosso teste verifique também se o resultado da função cast_to_int() retornou um inteiro, podemos fazer da seguinte maneira:

Testes unitários com classes

Uma boa prática é encapsular os testes unitários de uma função em uma classe.

Aplicando ao nosso teste unitário, temos que

Conclusão

Desenvolver testes unitários para suas funções é uma ótima maneira de economizar tempo, prever bugs e aumentar a confiança para suas funções, então comece agora mesmo a adotar essa prática no seus projetos!

--

--