Noções básicas do Pytest

Concluído

Vamos começar a testar com o Pytest. Como mencionamos na unidade anterior, o Pytest é altamente configurável e pode lidar com conjuntos de testes complexos. Mas não é preciso muito para começar a escrever testes. Na verdade, quanto mais fácil uma estrutura permitir que você escreva testes, melhor.

No final desta seção, você deve ter tudo o que precisa para começar a escrever seus primeiros testes e executá-los com o Pytest.

Convenções

Antes de mergulhar em testes de redação, devemos abordar algumas das convenções de teste nas quais o Pytest se baseia.

Não há regras rígidas sobre arquivos de teste, diretórios de teste ou layouts de teste gerais em Python. Ao conhecer essas regras, você pode aproveitar a descoberta e a execução automáticas de testes sem a necessidade de qualquer configuração extra.

Diretório de testes e arquivos de teste

O diretório principal para testes é o diretório tests . Você pode colocar esse diretório no nível raiz do projeto, mas também não é incomum vê-lo ao lado de módulos de código.

Nota

Neste módulo, usaremos como padrão testes na raiz de um projeto.

Vamos ver como fica a raiz de um pequeno projeto 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 único arquivo de teste. Nesse caso, o arquivo de teste é chamado de test_main.py. Este exemplo demonstra duas convenções críticas:

  • Use um diretório de testes para colocar arquivos de teste e diretórios de teste aninhados.
  • Arquivos de teste de prefixo com teste. O prefixo indica que o arquivo contém código de teste.

Atenção

Evite usar test (formulário singular) como o nome do diretório. O test nome é um módulo Python, portanto, criar um diretório chamado o mesmo o substituiria. Em vez disso, use sempre o plural tests .

Testar funções

Um dos fortes argumentos para usar o Pytest é que ele permite que você escreva funções de teste. Semelhante aos arquivos de teste, as funções de teste devem ser prefixadas com test_. O test_ prefixo garante que Pytest coleta o teste e o executa.

Veja como é uma função de teste simples:

def test_main():
    assert "a string value" == "a string value"

Nota

Se você está familiarizado com unittest, pode ser surpreendente ver o uso de na função de assert teste. Abordaremos afirmações simples com mais detalhes mais tarde, mas com o Pytest, você obtém relatórios de falhas ricos com afirmações simples.

Classes de ensaio e métodos de ensaio

Semelhante às convenções para arquivos e funções, classes de teste e métodos de teste usam as seguintes convenções:

  • As classes de teste são prefixadas com Test
  • Os métodos de teste são prefixados com test_

Uma diferença central com a biblioteca do unittest Python é que não há necessidade de herança.

O exemplo a seguir usa esses prefixos e outras convenções de nomenclatura Python para classes e métodos. Ele demonstra uma pequena classe de teste que está verificando 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 corredor de teste. O executor de teste é um executável na linha de comando que, em um alto nível, pode:

  • Execute a coleção de testes localizando 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 falhas, erros e aprovação em testes.
  • Forneça relatórios detalhados no final de uma execução de teste.

Nota

Como o Pytest é uma biblioteca externa, ele deve ser instalado para poder usá-lo.

Dado esse conteúdo em um arquivo test_main.py , podemos ver como o Pytest se comporta executando os testes:

# contents of test_main.py file

def test_main():
    assert True

Na linha de comando, no mesmo caminho onde o arquivo test_main.py existe, podemos executar o pytest executável:

 $ 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 qualquer configuração necessária.

A poderosa afirmação

Até agora, nossos exemplos de teste estão todos usando a chamada simples assert . Normalmente, em Python, a instrução não é usada para testes porque carece de relatórios adequados quando a assert asserção falha. Pytest, no entanto, não tem essa limitação. Nos bastidores, o Pytest está permitindo que a instrução realize comparações ricas sem forçar o usuário a escrever mais código ou configurar nada.

Usando a instrução plain assert , você pode fazer uso dos operadores do Python. Por exemplo: >, , , >=<!=, ou <=. Todos os operadores do Python são válidos. Esse recurso pode ser o recurso mais crucial do Pytest: você não precisa aprender uma nova sintaxe para escrever asserções.

Vamos ver como isso se traduz ao lidar com comparações comuns com objetos Python. Neste caso, vamos analisar o relatório de falhas ao comparar cadeias 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 contexto útil em torno da falha. Um invólucro incorreto no início da cadeia de caracteres e um caractere extra em uma palavra. Mas além das strings, o 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 da lista) é diferente. Não só identifica o número do índice, como também fornece uma representação da falha. Além de comparações de itens, ele também pode relatar se os itens estão faltando e fornecer informações que podem dizer exatamente qual item pode ser. No seguinte caso, isto é "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, vejamos como se comporta com os dicionários. Comparar dois grandes dicionários pode ser esmagador se houver falhas, mas Pytest faz um excelente trabalho em fornecer 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 "street" valor é diferente, e a outra é que "number" não corresponde.

O Pytest está detetando 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 conteúdo relevante. Vamos ver o que acontece se fizermos uso da bandeira sugerida -vv para aumentar a verbosidade 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 -vvo , o relatório aumenta a quantidade de detalhes e fornece uma comparação granular. Este relatório não apenas deteta e mostra a falha, mas também permite que você faça alterações rapidamente para corrigir o problema.