Noções básicas de Pytest
Vamos começar a testar usando Pytest. Como mencionamos na unidade anterior, o Pytest é altamente configurável e pode lidar com conjuntos de testes complexos, mas não requer muito para começar a escrever testes. De fato, quanto maior a facilidade para escrever testes em uma estrutura, melhor.
Ao final desta seção, você terá tudo o que precisa para começar a gravar seus primeiros testes e executá-los com o Pytest.
Convenções
Antes de mergulhar na escrita de testes, precisamos tratar de algumas das convenções de teste nas quais Pytest se baseia.
Não há muitas regras rígidas sobre arquivos de teste, diretórios de teste ou layouts gerais de teste no Python. Conhecendo essas regras, você pode aproveitar a detecção e a execução de testes automáticas sem necessidade de nenhuma configuração extra.
Diretório de testes e arquivos de teste
O diretório principal para os testes é o diretório tests. Você pode posicionar esse diretório no nível raiz do projeto, mas também não é incomum vê-lo ao lado dos módulos de código.
Observação
Neste módulo, usaremos testes na raiz de um projeto.
Vamos ver como fica a raiz de um pequeno projeto em Python chamado jformat
:
.
├── README.md
├── jformat
│ ├── __init__.py
│ └── main.py
├── setup.py
└── tests
└── test_main.py
O diretório tests está na raiz do projeto com um só arquivo de teste. Nesse caso, o arquivo de teste é chamado test_main.py. Este exemplo demonstra duas convenções críticas:
- Use um diretório testes para posicionar arquivos de teste e diretórios de teste aninhados.
- Usar o prefixo test nos arquivos de teste. O prefixo indica que o arquivo contém código de teste.
Cuidado
Evite usar test
(forma singular) como nome do diretório. O nome test
é um módulo de Python, portanto, criar um diretório com o mesmo nome o substituiria. Em vez disso, use sempre o plural tests
.
Testar funções
Um argumento forte para usar o Pytest é que ele permite que você escreva funções de teste. De modo semelhante aos arquivos de teste, as funções de teste devem ser prefixadas com test_
. O prefixo test_
garante que o Pytest colete o teste e o execute.
Esta é a aparência de uma função de teste simples:
def test_main():
assert "a string value" == "a string value"
Observação
Se você estiver familiarizado com unittest
, talvez seja surpreendente ver o uso de assert
na função de teste. Abordaremos as instruções de declaração simples em mais detalhes posteriormente, mas com o Pytest, você obtém um avançado relatório de falhas com instruções de declaração simples.
Classes de teste e métodos de teste
De modo semelhante às convenções para arquivos e funções, as classes de teste e os métodos de teste usam as seguintes convenções:
- As classes de teste têm o prefixo
Test
- Os métodos de teste têm o prefixo
test_
Uma das principais diferenças em relação à biblioteca unittest
do Python é que não existe a necessidade de herança.
O exemplo a seguir usa esses prefixos e outras convenções de nomenclatura do Python para classes e métodos. Ele demonstra uma pequena classe de teste que verifica nomes de usuário em um aplicativo.
class TestUser:
def test_username(self):
assert default() == "default username"
Executar testes
Pytest é uma estrutura de teste e um executor de testes. O test runner é um executável na linha de comando que, de forma geral, pode:
- Realize a coleção de testes encontrando todos os arquivos de teste, classes de teste e funções de teste para uma execução de teste.
- Inicie uma execução de teste executando todos os testes.
- Acompanhe as falhas, os erros e os testes aprovados.
- Forneça relatórios avançados ao final de uma execução de teste.
Observação
Como o Pytest é uma biblioteca externa, ele deve ser instalado para usá-la.
Considerando esse conteúdo em um arquivo test_main.py, podemos ver como Pytest se comporta executando os testes:
# contents of test_main.py file
def test_main():
assert True
Na linha de comando, no mesmo caminho em que existe o arquivo test_main.py, podemos executar o executável pytest
:
$ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item
test_main.py . [100%]
============================ 1 passed in 0.00s =============================
Nos bastidores, o Pytest coleta o teste de exemplo no arquivo de teste sem precisar de nenhuma configuração.
A poderosa instrução assert
Até agora, nossos exemplos de teste estão todos usando a chamada assert
simples. Normalmente, no Python, a instrução assert
não é usada para testes, pois não tem relatórios adequados quando a declaração falha. O Pytest, entretanto, não tem essa limitação. Nos bastidores, o Pytest permite que a instrução execute comparações avançadas sem forçar o usuário a escrever mais código ou configurar qualquer coisa.
Usando a instrução simples assert
, você pode usar os operadores do Python; por exemplo, , >
, <
, !=
, >=
ou <=
. Todos os operadores de Python são válidos. Esta funcionalidade pode ser o recurso mais crucial de Pytest: você não precisa aprender uma nova sintaxe para escrever declarações.
Vamos ver como isso se traduz ao lidar com comparações comuns com objetos de Python. Neste caso, vamos examinar o relatório de falhas ao comparar cadeia de caracteres longas:
================================= FAILURES =================================
____________________________ test_long_strings _____________________________
def test_long_strings():
left = "this is a very long strings to be compared with another long string"
right = "This is a very long string to be compared with another long string"
> assert left == right
E AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E - This is a very long string to be compared with another long string
E ? ^
E + this is a very long strings to be compared with another long string
E ? ^ +
test_main.py:4: AssertionError
O Pytest mostra o contexto útil em torno da falha: uma caixa incorreta no início da cadeia e um caractere extra em uma palavra. Mas, além das cadeias de caracteres, Pytest pode ajudar com outros objetos e estruturas de dados. Por exemplo, veja como ele se comporta com listas:
________________________________ test_lists ________________________________
def test_lists():
left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
> assert left == right
E AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E At index 1 diff: 'wheat' != 'coffee'
E Full diff:
E - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E ? ---------
E + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E ? +++++++++
test_main.py:9: AssertionError
Este relatório identifica que o índice 1 (segundo item na lista) é diferente. Ele não apenas identifica o número do índice, mas também fornece uma representação da falha. Além das comparações entre itens, ele também pode relatar se itens estão ausentes e fornecer informações que podem informar exatamente qual pode ser o item. No caso a seguir, isso será "milk"
:
________________________________ test_lists ________________________________
def test_lists():
left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
right = ["sugar", "wheat", "salt", "water", "milk"]
> assert left == right
E AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E At index 2 diff: 'coffee' != 'salt'
E Left contains one more item: 'milk'
E Full diff:
E - ['sugar', 'wheat', 'salt', 'water', 'milk']
E + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E ? ++++++++++
test_main.py:9: AssertionError
Por fim, vamos ver como ele se comporta com dicionários. A comparação de dois dicionários grandes pode ser esmagadora se existirem falhas, mas o Pytest faz um excelente trabalho ao fornecer o contexto e identificar a falha:
____________________________ test_dictionaries _____________________________
def test_dictionaries():
left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
> assert left == right
E AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E Omitting 3 identical items, use -vv to show
E Differing items:
E {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E {'number': 39} != {'number': 38}
E Full diff:
E {
E 'county': 'Frett',...
E
E ...Full output truncated (12 lines hidden), use '-vv' to show
Neste teste, há duas falhas no dicionário. Uma é que o valor de "street"
é diferente, e outra é que "number"
não corresponde.
Pytest detecta com precisão essas diferenças (mesmo que seja uma falha em um único teste). Como os dicionários contêm muitos itens, o Pytest omite as partes idênticas e mostra apenas conteúdo relevante. Vamos ver o que acontece quando usamos o sinalizador -vv
sugerido para aumentar o detalhamento na saída:
____________________________ test_dictionaries _____________________________
def test_dictionaries():
left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
> assert left == right
E AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E Common items:
E {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E Differing items:
E {'number': 39} != {'number': 38}
E {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E Full diff:
E {
E 'county': 'Frett',
E - 'number': 38,
E ? ^
E + 'number': 39,
E ? ^
E 'state': 'Nevada',
E - 'street': 'Ferry Lane',
E ? - ^
E + 'street': 'Ferry Ln.',
E ? ^
E 'zipcode': 30877,
E }
Ao executar pytest -vv
, o relatório aumenta a quantidade de detalhes e fornece uma comparação granular. Esse relatório não apenas detecta e mostra a falha, mas também permite que você faça alterações rapidamente para corrigir o problema.