Noções básicas de Pytest

Concluído

Vamos começar a testar usando Pytest. Como mencionamos na unidade anterior, o Pytest é altamente configurável e pode tratar pacotes de testes complexos. Mas ele não exige muito para você 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á regras rígidas sobre arquivos de teste, diretórios de teste ou layouts de teste em geral 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, adotaremos o padrão de deixar tests 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 dos argumentos fortes para usar o Pytest é que ele permite gravar 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ê está familiarizado com unittest, pode ser 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 executor de testes é um executável na linha de comando que, em alto nível, 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 Pytest é uma biblioteca externa, ele precisa ser instalado para ser usado.

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 porque não há um relato adequado quando a declaração falha. O Pytest, entretanto, não tem essa limitação. Nos bastidores, Pytest está habilitando a instrução a executar comparações avançadas sem forçar o usuário a escrever mais código ou configurar algo.

Usando a instrução assert sem formatação, você pode usar os operadores de 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

Pytest mostra um contexto útil em torno da falha. O uso incorreto de maiúsculas e minúsculas no início da cadeia de caracteres 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.

O Pytest está detectando com precisão essas diferenças (mesmo que seja uma falha em um único teste). Como os dicionários contêm muitos itens, Pytest omite as partes idênticas e mostra apenas o 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.