Course
O teste é um tópico importante no desenvolvimento de software. Antes de um produto de software chegar às mãos de um usuário final, é provável que ele tenha passado por vários testes, como testes de integração, testes de sistemas e testes de aceitação. A ideia por trás desses testes rigorosos é garantir que o comportamento do aplicativo esteja funcionando conforme o esperado do ponto de vista dos usuários finais. Essa abordagem de teste é conhecida como desenvolvimento orientado por comportamento (BDD).
Mais recentemente, o interesse pelo desenvolvimento orientado por testes (TDD) cresceu significativamente entre os desenvolvedores. Aprofundar o assunto pode ser uma tarefa complexa para este artigo, mas a ideia geral é que o processo tradicional de desenvolvimento e teste seja invertido: primeiro você escreve os testes unitários e, em seguida, implementa as alterações no código até que os testes sejam aprovados.
Neste artigo, vamos nos concentrar nos testes de unidade e, especificamente, em como fazê-los usando uma estrutura de teste Python popular chamada Pytest.
O que são testes de unidade?
Os testes unitários são uma forma de testes automatizados, o que significa simplesmente que o plano de teste é executado por um script em vez de manualmente por um ser humano. Eles servem como o primeiro nível de teste de software e geralmente são escritos na forma de funções que validam o comportamento de várias funcionalidades em um programa de software.
Os níveis de teste de software
A ideia por trás desses testes é permitir que os desenvolvedores isolem a menor unidade de código que faça sentido lógico e testem se ela se comporta conforme o esperado. Em outras palavras, os testes de unidade validam que o componente único de um programa de software funciona como os desenvolvedores pretendiam.
O ideal é que esses testes sejam bem pequenos - quanto menores forem, melhor. Um motivo para criar testes menores é que o teste será mais eficiente, pois testar unidades menores permitirá que o código de teste seja executado muito mais rapidamente. Outro motivo para testar componentes menores é que isso lhe dá uma visão melhor de como o código granular se comporta quando mesclado.
Por que precisamos de testes unitários?
A justificativa global para a necessidade de realizar testes unitários é que os desenvolvedores devem garantir que o código que escrevem atenda aos padrões de qualidade antes de permitir que ele entre em um ambiente de produção. Entretanto, vários outros fatores contribuem para a necessidade de testes unitários. Vamos nos aprofundar em alguns desses motivos.
Preserva os recursos
A realização de testes unitários ajuda os desenvolvedores a detectar erros de código durante o estágio de construção do software, evitando que eles se aprofundem no ciclo de vida do desenvolvimento. Isso preserva os recursos, pois os desenvolvedores não precisariam arcar com o custo de corrigir bugs posteriormente no desenvolvimento, além de significar que os usuários finais têm menos probabilidade de lidar com códigos com bugs.
Documentação extra
Outra grande justificativa para a realização de testes unitários é que eles servem como uma camada extra de documentação viva para o seu produto de software. Os desenvolvedores podem simplesmente consultar os testes de unidade para obter uma compreensão holística do sistema geral, pois eles detalham como os componentes menores devem se comportar.
Aumento da confiança
É extremamente simples cometer erros sutis em seu código ao escrever alguma funcionalidade. No entanto, a maioria dos desenvolvedores concordaria que é muito melhor identificar os pontos de ruptura em uma base de código antes de colocá-la em um ambiente de produção: os testes de unidade oferecem essa oportunidade aos desenvolvedores.
É justo dizer que "o código coberto por testes de unidade pode ser considerado mais confiável do que o código que não é". Futuras falhas no código podem ser descobertas muito mais rapidamente do que no código sem cobertura de teste, economizando tempo e dinheiro. Os desenvolvedores também se beneficiam da documentação extra para que possam entender a base de código mais rapidamente, e há a confiança adicional de saber que, se cometerem um erro em seu código, ele será detectado pelos testes de unidade e não por um usuário final.
Estruturas de teste Python
A popularidade do Python cresceu tremendamente ao longo dos anos. Como parte do crescimento do Python, o número de estruturas de teste também aumentou, resultando em uma grande quantidade de ferramentas disponíveis para ajudá-lo a testar seu código Python. Entrar nos detalhes de cada ferramenta está além do escopo deste artigo, mas abordaremos alguns dos frameworks de teste Python mais comuns disponíveis.
teste único
O Unittest é uma estrutura Python integrada para testes de unidade. Ele foi inspirado em uma estrutura de teste de unidade chamada JUnit da linguagem de programação Java. Como ele vem pronto para uso com a linguagem Python, não há módulos extras a serem instalados, e a maioria dos desenvolvedores o utiliza para começar a aprender sobre testes.
Pytest
O Pytest é possivelmente a estrutura de teste em Python mais usada, o que significa que ele tem uma grande comunidade para ajudá-lo sempre que você tiver problemas. É uma estrutura de código aberto que permite que os desenvolvedores criem conjuntos de testes simples e compactos, além de oferecer suporte a testes unitários, testes funcionais e testes de API.
doctest
A estrutura doctest mescla dois componentes principais da engenharia de software: documentação e teste. Essa funcionalidade garante que todos os programas de software sejam minuciosamente documentados e testados para assegurar que sejam executados como deveriam. O doctest vem com a biblioteca padrão do Python e é bastante simples de aprender.
nose2
O Nose2, sucessor do regimento do nose, é essencialmente um unittest com plug-ins. As pessoas geralmente se referem ao nose2 como "testes de unidade estendidos" ou "testes de unidade com um plug-in" devido a seus laços estreitos com a estrutura de testes de unidade integrada do Python. Como é praticamente uma extensão da estrutura do unittest, o nose2 é incrivelmente fácil de adotar para aqueles que estão familiarizados com o unittest.
Testemunhar
O Testify, uma estrutura Python para testes de unidade, integração e sistema, é popularmente conhecido como a estrutura que foi projetada para substituir o unittest e o nose. A estrutura está repleta de plug-ins abrangentes e tem uma curva de aprendizado bastante suave se você já estiver familiarizado com o unittest.
Hipótese
O Hypothesis permite que os desenvolvedores criem testes de unidade que são mais simples de escrever e são poderosos quando executados. Como a estrutura foi criada para dar suporte a projetos de ciência de dados, ela ajuda a encontrar casos extremos que não são tão aparentes enquanto você cria seus testes, gerando exemplos de entradas que se alinham com propriedades específicas que você define.
Em nosso tutorial, usaremos o pytest. Confira a próxima seção para ver por que você pode optar pelo Pytest em vez dos outros que listamos.
Por que usar o Pytest?
Além de sua vasta comunidade de suporte, o pytest tem vários fatores que o tornam uma das melhores ferramentas para conduzir seu conjunto de testes automatizados em Python. A filosofia e os recursos do Pytest foram criados para tornar o teste de software uma experiência muito melhor para o desenvolvedor. Uma das maneiras pelas quais os criadores do Pytest atingiram esse objetivo foi reduzindo significativamente a quantidade de código necessária para executar tarefas comuns e possibilitando a execução de tarefas avançadas com comandos e plug-ins abrangentes.
Alguns outros motivos para usar o Pytest são os seguintes:
Fácil de aprender
O Pytest é extremamente fácil de aprender: se você entender como funciona a palavra-chave assert do Python, já estará no caminho certo para dominar a estrutura. Os testes que usam o pytest são funções Python com "test_" precedido ou "_test" anexado ao nome da função, embora você possa usar uma classe para agrupar vários testes. Em geral, a curva de aprendizado do pytest é muito mais rasa do que a do unittest, pois você não precisa aprender nenhuma construção nova.
Filtragem de teste
Talvez você não queira executar todos os seus testes a cada execução - esse pode ser o caso à medida que seu conjunto de testes cresce. Às vezes, você pode querer isolar alguns testes em um novo recurso para obter feedback rápido durante o desenvolvimento e, em seguida, executar o conjunto completo quando tiver certeza de que tudo está funcionando conforme o planejado. O Pytest tem três maneiras de isolar os testes: 1) filtragem baseada em nome, que diz ao pytest para executar somente os testes cujos nomes correspondem ao padrão fornecido 2) escopo de diretório, que é uma configuração padrão que diz ao pytest para executar somente os testes que estão dentro ou sob o diretório atual e 3) categorização de teste que permite que você defina categorias para testes que o pytest deve incluir ou excluir.
Parametrização
O Pytest tem um decorador interno chamado parametrizar que permite a parametrização de argumentos para uma função de teste. Assim, se as funções que você estiver testando processarem dados ou realizarem uma transformação genérica, não será necessário escrever vários testes semelhantes. Falaremos mais sobre parametrização mais adiante neste artigo.
Vamos parar por aqui, mas a lista de motivos pelos quais o pytest é uma ótima opção de ferramenta para seu conjunto de testes automatizados continua.
Pytest vs unittest
Apesar de todos os motivos que abordamos acima, alguém ainda pode contestar a ideia de usar o pytest pelo simples fato de ser uma estrutura de terceiros - "qual é o sentido de instalar uma estrutura se já existe uma integrada?" É um bom argumento, mas para nos protegermos nessa disputa, forneceremos alguns pontos a serem considerados.
Observação: Se você já está convencido sobre o pytest, pule para a próxima seção, onde aprenderemos a usar a estrutura.
Menos clichês
O Unittest exige que os desenvolvedores criem classes derivadas do módulo TestCase e, em seguida, definam os casos de teste como métodos na classe.
"""
An example test case with unittest.
See: https://docs.python.org/3/library/unittest.html
"""
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
O Pytest, por outro lado, exige apenas que você defina uma função com "test_" anexado e use as condições de asserção dentro delas.
"""
An example test case with pytest.
See: https://docs.pytest.org/en/6.2.x/index.html
"""
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
Observe a diferença na quantidade de código necessário; o unittest tem uma quantidade significativa de código padrão necessário, que serve como requisito mínimo para qualquer teste que você queira executar. Isso significa que é muito provável que você acabe escrevendo o mesmo código várias vezes. O Pytest, por outro lado, tem ricos recursos embutidos que simplificam esse fluxo de trabalho, reduzindo a quantidade de código necessária para escrever casos de teste.
Saída
Os resultados fornecidos por cada estrutura são extremamente diferentes. Aqui está um exemplo de execução do pytest:
"""
See: https://docs.pytest.org/en/6.2.x/index.html
"""
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item
test_sample.py F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================
O caso de teste acima falhou, mas observe o detalhamento da falha. Isso torna mais fácil para os desenvolvedores identificar onde estão os erros em seu código, o que é muito útil na depuração. Como bônus adicional, há também um relatório de status geral do conjunto de testes, que nos informa o número de testes que falharam e quanto tempo levou.
Vamos dar uma olhada em um exemplo de caso de teste com falha com o unittest.
import unittest
def square(n):
return n*n
def cube(n):
return n*n*n
class TestCase(unittest.TestCase):
def test_square(self):
self.asserEquals(square(4), 16)
def test_cube(self):
self.assertEquals(cube(4), 16)
Quando executamos o script, obtemos o seguinte resultado:
---------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (errors=1)
A saída acima nos informa que dois testes foram executados em 0,001s e um falhou, mas não muito mais. Por fim, o pytest fornece feedback muito mais informativo, o que é útil na hora de depurar.
Em suma, tanto o pytest quanto o unittest são ótimas ferramentas para testes automatizados em Python. Vários desenvolvedores de Python podem se inclinar mais para o pytest do que para seus equivalentes devido à sua compactação e eficiência. Também é extremamente fácil de adotar e há vários recursos que você pode usar para criar um conjunto de testes eficaz.
Agora vamos à parte principal deste artigo. Discutimos o que é teste de unidade e por que o pytest é uma ótima ferramenta para testes automatizados em Python. Agora vamos dar uma olhada em como usar a ferramenta.
Tutorial do Pytest
Vamos ver como funciona essa estrutura de teste do Python sobre a qual estamos falando.
A primeira etapa é instalar o pacote, o que pode ser feito com um simples comando pip.
Observação: Os criadores do pytest recomendam que você use o venv para desenvolvimento e o pip para instalar seu aplicativo, dependências e o próprio pytest.
pip install -U pytest
Em seguida, verifique se a estrutura foi instalada usando o seguinte comando:
>>>> pytest --version
pytest 7.1.2
Tudo está instalado. Agora você está pronto para começar a executar alguns testes.
Criação de um teste simples
Criar um teste é simples com o Pytest. Para demonstrar essa funcionalidade, criamos um script chamado calcualte_age.py. Esse script tem apenas uma função, get_age, que é responsável por calcular a idade de um usuário, considerando sua data de nascimento.
import datetime
def get_age(yyyy:int, mm:int, dd:int) -> int:
dob = datetime.date(yyyy, mm, dd)
today = datetime.date.today()
age = round((today - dob).days / 365.25)
return age
O Pytest executará todos os arquivos python que tenham o nome test_ prefixado ou _test anexado ao nome do script. Para ser mais específico, o pytest segue as seguintes convenções para a descoberta de testes [fonte: documentação]:
- Se nenhum argumento for especificado, a coleta do pytest começará em testpaths se eles estiverem configurados: testpaths é uma lista de diretórios que o pytest pesquisará quando nenhum diretório, arquivo ou ID de teste específico for fornecido.
- O Pytest recursaria então os diretórios, a menos que você tenha dito para não fazê-lo definindo norecursedirs; ele está procurando arquivos que começam com test_*.py ou terminam em *_test.py
- Nesses arquivos, o pytest coletaria itens de teste na seguinte ordem:
- Funções ou métodos de teste com prefixo fora da classe
- Funções ou métodos de teste com prefixo dentro de classes de teste com prefixo de teste que não têm um método __init__.
Não especificamos nenhum argumento, mas criamos outro script no mesmo diretório chamado test_calculate_age.py: assim, quando os diretórios forem recursados, o teste será descoberto. Nesse script, temos um único teste, test_get_age, para validar se a nossa função está funcionando adequadamente.
Observação: Você pode decidir colocar seus testes em um diretório extra fora do aplicativo, o que é uma boa ideia se tiver vários testes funcionais ou se quiser manter o código de teste e o código do aplicativo separados por algum outro motivo.
from calculate_age import get_age
def test_get_age():
# Given.
yyyy, mm, dd = map(int, "1996/07/11".split(""))
# When.
age = get_age(yyyy, mm, dd)
# Then.
assert age == 26
Para executar o teste, execute o seguinte comando no prompt de comando:
py -m pytest
A saída de uma execução bem-sucedida do pytest.
E isso é tudo.
Mas e se precisarmos fornecer alguns dados para que nossos testes sejam aprovados? Diga olá aos acessórios do pytest.
Dispositivos Pytest
O inestimável recurso de acessórios do Pytest permite que os desenvolvedores alimentem os testes com dados. Basicamente, são funções que são executadas antes de cada função de teste para gerenciar o estado dos nossos testes. Por exemplo, digamos que temos vários testes que usam os mesmos dados, então podemos usar uma fixação para extrair os dados repetidos usando uma única função.
import pytest
from sklearn.model_selection import train_test_split
from fraud_detection_model.config.core import config
from fraud_detection_model.processing.data_manager import (
load_datasets,
load_interim_data
)
@pytest.fixture(scope="session")
def pipeline_inputs():
# import the training dataset
dataset = load_datasets(transaction=config.app_config.train_transaction, identity=config.app_config.train_identity, )
# divide train and test
X_train, X_test, y_train, y_test = train_test_split(
dataset[config.model_config.all_features], dataset[config.model_config.target],
test_size=config.model_config.test_size,
stratify=dataset[config.model_config.target],
random_state=config.model_config.random_state)
return X_train, X_test, y_train, y_test
No código acima, criamos um dispositivo usando o decorador pytest.fixture. Observe que o escopo está definido como "sessão" para informar ao pytest que queremos que o dispositivo seja destruído no final da sessão de teste.
Observação: nossos equipamentos são armazenados em conftest.py.
O código usa uma função que importamos de outro módulo para carregar dois arquivos csv na memória e mesclá-los em um único conjunto de dados. Depois disso, o conjunto de dados é dividido em conjuntos de treinamento e teste e retornado da função.
Para usar esse acessório em nossos testes, devemos passá-lo como um parâmetro para nossa função de teste. Neste exemplo, use nosso acessório em nosso script de teste test_pipeline.py da seguinte forma:
import pandas as pd
from pandas.api.types import is_categorical_dtype, is_object_dtype
from fraud_detection_model import pipeline
from fraud_detection_model.config.core import configfrom fraud_detection_model.processing.validation import validate_inputs
def test_pipeline_most_frequent_imputer(pipeline_inputs):
# Given
X_train, _, _, _ = pipeline_inputs assert all( x in X_train.loc[:, X_train.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
# When
X_transformed = pipeline.fraud_detection_pipe[:1].fit_transform(X_train[:50])
# Then
assert all( x not in X_transformed.loc[:, X_transformed.isnull().any()].columns for x in config.model_config.impute_most_freq_cols )
def test_pipeline_aggregate_categorical(pipeline_inputs):
# Given
X_train, _, _, _ = pipeline_inputs
assert X_train["R_emaildomain"].nunique() == 60
# When
X_transformed = pipeline.fraud_detection_pipe[:2].fit_transform(X_train[:50])
# Then
assert X_transformed["R_emaildomain"].nunique() == 2
Não se preocupe muito com o que o código está fazendo. O mais importante que queremos destacar é como reduzimos significativamente a necessidade de escrever código redundante porque criamos um dispositivo que passamos como parâmetro para extrair dados em nossos testes.
No entanto, há situações em que as luminárias podem ser um exagero. Por exemplo, se os dados que estão sendo inseridos em seus testes precisarem ser processados novamente em cada caso de teste, isso é praticamente o equivalente a encher seu código com vários objetos simples. Com tudo isso dito, os acessórios provavelmente desempenharão um papel fundamental em seu conjunto de testes, mas discernir quando usá-los ou evitá-los exigirá prática e muita reflexão.
Parametrização do Pytest
As luminárias são ótimas quando você tem vários testes com as mesmas entradas. E se você quiser testar uma única função com pequenas variações nas entradas? Uma solução é escrever vários testes diferentes com vários casos.
def test_eval_addition():
assert eval("2 + 2") == 4
def test_eval_subtraction():
assert eval("2 - 2") == 0
def test_eval_multiplication():
assert eval("2 * 2") == 4
def test_eval_division():
assert eval("2 / 2") == 1.0
Embora essa solução certamente funcione, ela não é a mais eficiente: para começar, há muito código padrão. Uma solução melhor é usar o decorador pytest.mark.parametrize() para habilitar a parametrização de argumentos para uma função de teste. Isso nos permitirá definir uma única definição de teste e, em seguida, o pytest testará os vários parâmetros que especificarmos para nós.
Veja como reescreveríamos o código acima se usássemos a parametrização:
import pytest
@pytest.mark.parametrize("test_input, expected_output", [("2+2", 4), ("2-2", 0), ("2*2", 4), ("2/2", 1.0)])
def test_eval(test_input, expected_output):
assert eval(test_input) == expected_output
O decorador @parametrize define quatro entradas de teste diferentes e valores esperados para a execução da função test_eval, o que significa que a função será executada quatro vezes usando cada uma delas.
A saída do pytest para o teste de parametrização
Neste artigo, abordamos os seguintes tópicos:
- o que são testes unitários
- por que precisamos de testes unitários
- diferentes estruturas de teste em python
- Por que o pytest é tão útil
- como usar o pytest e dois de seus principais recursos (fixtures e parametrização)
Agora você sabe o suficiente para começar a escrever seu próprio teste usando a estrutura pytest. É altamente recomendável que você faça isso, para que tudo o que aprendeu neste artigo seja mantido. Você também deve conferir nosso curso sobre teste de unidade para ciência de dados em python para obter uma explicação mais detalhada com exemplos.
Cursos para Python
Course
Intermediate Python
Course
Introduction to Data Science in Python
blog
5 desafios Python para desenvolver suas habilidades
DataCamp Team
5 min
blog
6 práticas recomendadas de Python para um código melhor
tutorial
21 ferramentas essenciais do Python
tutorial
Uso do PostgreSQL em Python
tutorial
Tutorial de iteradores e geradores Python
tutorial