Os testes desempenham um papel crucial no desenvolvimento de software, garantindo que o código funcione conforme o esperado. Eles consistem em escrever e executar scripts automatizados que verificam o comportamento do código em diferentes cenários. A importância dos testes reside em sua capacidade de detectar e corrigir erros precocemente, promovendo a confiabilidade, robustez e manutenibilidade do software.

Pytest

editar

O pytest é uma framework open-source de teste para Python que provê soluções para executar testes e fazer validações diversas, com a possibilidade de estender com plugins e até rodar testes do próprio unittest do Python.

Ele possui diversos pontos positivos, como sintaxe simples, permite a seleção de conjuntos de testes que se deseja que sejam realizados e realizar testes múltiplos em paralelo, entre outros.

Instação

editar

Para distribuições Arch Based podemos utilizar o seguinte comando:

sudo pacman -S python-pytest


Já para instalar em distribuições Debian Based, precisamos ter uma dependência, o pip do Python, e posteriormente utilizar o seguinte comando do pip:

sudo apt install python3-pip -y
pip install pytest


Para verificar se a instalação e versão do pytest:

pytest --version

Identifcação de funções e arquivos de teste

editar

Rodar o pytest sem especificar o nome do arquivo fará com que o comando rode todos os arquivos que sigam o padrão

test_*.py ou _test.py no diretório atual e seus subdiretórios, ou seja, o pytest automaticamente identifica arquivos desse formato como arquivos de teste. Podemos também fazer com que o pytest rode outros arquivos explicicitamente mencionando eles.

Além disso, o pytest requer que as funções teste se iniciem com "test". Funções que não seguem esse formato não são consideradas como funções de teste pelo pytest. Não é possível fazer com que o pytest considere uma função que não segue o formato padrão como uma função de teste.

Teste simples

editar

Podemos então criar um diretório onde vamos adicionar nossos arquivos de teste.

Nele vamos criar o seguinte arquivo de exemplo nomeado como test_square.py:

import math

def test_sqrt():
   num = 25
   assert math.sqrt(num) == 5

def testsquare():
   num = 7
   assert 7**2 == 40

def tesequality():
   assert 10 == 11

Podemos então rodar o seguinte comando, que executa todos os arquivos de teste presentes no diretório atual e em seus subdiretórios.

pytest


Como nosso diretório atualmente possui apenas um arquivo de teste, ele será o único a ser executado. Mas caso tivéssemos outros arquivos, poderíamos selecionar o desejado utilizado o seguinte comando:

pytest <filename>

Neste caso, usáriamos no nome do nosso arquivo, test_square.py. Temos então a seguinte saída do comando:

test_square.py .F                                                                                [100%]

=============================================== FAILURES ===============================================
______________________________________________ testsquare ______________________________________________

    def testsquare():
       num = 7
>      assert 7**2 == 40
E      assert (7 ** 2) == 40

test_square.py:9: AssertionError
======================================= short test summary info ========================================
FAILED test_square.py::testsquare - assert (7 ** 2) == 40
===================================== 1 failed, 1 passed in 0.02s ======================================

Na primeira linha da saída encontramos as seguintes informações:

  1. Nome do arquivo;
  2. Indicação de quais testes deram errado e quais deram certo.
    1. F significa que a função falhou;
    2. . significa que o teste obteve sucesso.

Podemos notar pela indicação dos testes que falharam e dos testes que deram certo que a função test_sqrt() obteve sucesso e a testsquare() falhou, a função teseequality() não foi considerada uma função de teste por não seguir a formatação padrão do pytest, e, assim, não foi executada.

Posteriormente, podemos ver mais detalhes sobre os testes que falharam. Neste casso, vemos que a função testsquare() falhou no teste assert(7**2) == 40.

Podemos também utilizar a flag -v em conjunto com o comando pytest, que faz com que a saída do programa seja verbosa, indicando mais explicitamente quais testes deram certo e quais falharam.

É importante ressaltar que podemos ter vários asserts dentro de cada função de teste, contudo, a função de teste só executada até a sua primeira falha.

Seleção de testes

editar

Vamos criar mais um arquivo de teste chamado test_compare.py:

def test_greater():
   num = 100
   assert num > 100

def test_greater_equal():
   num = 100
   assert num >= 100

def test_less():
   num = 100
   assert num < 200

Somos capazes de selecionar quais testes queremos executar. Temos duas maneiras de fazer isso: Da primeira maneira, podemos selecionar os testes a partir de uma substring comum, com o seguinte comando:

pytest -k <substring> <flags>


Um exemplo seria utilizar o seguinte comando, que executa apenas as funções test_greater() e test_greater_equal():

pytest -k great -v

Podemos também selecionar os testes a partir de markers.

Os markers são utilizados para setar atributos a funções teste, há alguns markers inbutidos, como xfail, skip e parametrize, que serão abordados ulteriormente. Contudo, conseguimos criar nossos próprios markers. Para podermos utilizar esses atributos, precisamos importar a biblioteca pytest.

A sintaxe da criação de um marker segue a seguinte formatação:

@pytest.mark.<markername>


Vamos então criar um novo arquivo test_string.py:

import pytest

@pytest.mark.string
def test_str_equal():
    str1 = "oiee"
    str2 = "tchauu"
    assert str1==str2
    
@pytest.mark.string
def test_str_diff():
    str1 = "imee"
    str2 = "imee"
    assert str1==str2
    
@pytest.mark.outros
def test_str_int():
    str1 = "1"
    num1 = 1
    assert str1 != num1


Para rodar somente os testes com um determinado marker, temos o seguinte comando de terminal:

pytest -m <markername>  <flags>


No nosso exemplo, podemos rodar o comando:

pytest -m string -v


Temos então o seguinte resultado somente das funções marcadas com o marker indicado:

test_string.py::test_str_equal FAILED                                                            [ 50%]
test_string.py::test_str_diff PASSED                                                             [100%]

=============================================== FAILURES ===============================================
____________________________________________ test_str_equal ____________________________________________

    @pytest.mark.string
    def test_str_equal():
        str1 = "oiee"
        str2 = "tchauu"
>       assert str1==str2
E       AssertionError: assert 'oiee' == 'tchauu'
E         - tchauu
E         + oiee

test_string.py:7: AssertionError
FAILED test_string.py::test_str_equal - AssertionError: assert 'oiee' == 'tchauu'
======================== 1 failed, 1 passed, 13 deselected, 3 warnings in 0.01s ========================

Fixtures

editar

Fixtures são funções que vão rodar antes de cada função teste a qual é aplicada. Podem ser utilizadas para conseguir dados para os testes, como por exemplo, conexão com databases, URLs para teste, e alguma ordenação do input.

Uma função é marcada como fixture usando @pytest.fixture. Podemos criar agora um arquivo nomeado test_div_by_3_6.py:

import pytest

@pytest.fixture
def input_value():
   input = 39
   return input

def test_divisible_by_3(input_value):
   assert input_value % 3 == 0

def test_divisible_by_6(input_value):
   assert input_value % 6 == 0


Neste exemplo, a função marcada como fixture é a input_value(), para as funções teste rodadem ela antes, vão precisar que cita-la como parâmetro.

Para rodar o arquivo, podemos utilizar o comando:

pytest test_div_by_3_6.py -v


Temos o seguinte resultado:

test_div_by_3_6.py::test_divisible_by_3 PASSED                                                   [ 50%]
test_div_by_3_6.py::test_divisible_by_6 FAILED                                                   [100%]

=============================================== FAILURES ===============================================
_________________________________________ test_divisible_by_6 __________________________________________

input_value = 39

    def test_divisible_by_6(input_value):
>      assert input_value % 6 == 0
E      assert (39 % 6) == 0

test_div_by_3_6.py:12: AssertionError
===================================== 1 failed, 1 passed in 0.01s ======================================

Podemos observar que as funções receberam o valor retornado pela função input_value().

Limitações e contest.py

editar

O escopo da função fixture é apenas o arquivo de teste em que foi definida. Ou seja, no exemplo apresentado anteriormente, a função input_value() só é reconhecida no arquivo test_div_by_3_6.py.

Para que uma função fixture seja visível para múltiplos arquivos temos que definir a função em um arquivo chamado conftest.py.

Vamos então alterar o arquivo test_div_by_3_6.py de modo a tirar a função fixture presente nele e coloca-la no arquivo conftest.py. Vamos também criar um novo arquivo nomeado test_div_by_13.py.

import pytest

def test_divisible_by_13(input_value):
   assert input_value % 13 == 0


Para realizar testes podemos rodar o seguinte comando que roda todas as funções que utilizam o retorno da função input_value():

pytest -k divisible -v


E obtemos então o seguinte resultado:

test_div_by_13.py::test_divisible_by_13 PASSED                                                   [ 33%]
test_div_by_3_6.py::test_divisible_by_3 PASSED                                                   [ 66%]
test_div_by_3_6.py::test_divisible_by_6 FAILED                                                   [100%]

=============================================== FAILURES ===============================================
_________________________________________ test_divisible_by_6 __________________________________________

input_value = 39

    def test_divisible_by_6(input_value):
>      assert input_value % 6 == 0
E      assert (39 % 6) == 0

test_div_by_3_6.py:12: AssertionError
======================== 1 failed, 2 passed, 12 deselected, 3 warnings in 0.01s ========================

Parametrizando testes:

editar

A parametrização de um teste é feita para rodar o teste com múltiplos conjuntos de inputs.

Para isso utilizamos o marker @pytest.mark.parametrize. Podemos criar o seguinte arquivo nomeado test_multiplication.py e rodar o teste indicado abaixo:

import pytest

@pytest.mark.parametrize("num, output",[(1,11),(2,22),(3,35),(4,44)])
def test_multiplication_11(num, output):
   assert 11*num == output
pytest -k multiplication -v


Com esse comando temos o seguinte resultado:

test_multiplication.py::test_multiplication_11[1-11] PASSED                                      [ 25%]
test_multiplication.py::test_multiplication_11[2-22] PASSED                                      [ 50%]
test_multiplication.py::test_multiplication_11[3-35] FAILED                                      [ 75%]
test_multiplication.py::test_multiplication_11[4-44] PASSED                                      [100%]

=============================================== FAILURES ===============================================
_____________________________________ test_multiplication_11[3-35] _____________________________________

num = 3, output = 35

    @pytest.mark.parametrize("num, output",[(1,11),(2,22),(3,35),(4,44)])
    def test_multiplication_11(num, output):
>      assert 11*num == output
E      assert (11 * 3) == 35

test_multiplication.py:5: AssertionError
======================== 1 failed, 3 passed, 11 deselected, 3 warnings in 0.01s ========================

Podemos notar que a função test_multiplication_11 foi testada com diferentes pares de inputs indicados no marker.

Markes imbutidos:

editar

O pytest irá executar a função marcada com @pytest.mark.xfail, contudo ela não será considerada como parte das funções que obtiveram sucesso ou falha. Os detalhes destes testes também não serão impressos, mesmo que eles falhem.

As funções marcadas com @pytest.mark.skip não serão executadas.


Estes markers são utilizadas quando um teste perde relevância ou quando algum atributo do seu código foi alterado e novos testes já foram escritos para esse atributo.

Executar até N falhas

editar

Podemos utilizar a flag --maxfail = <num> para indicar que desejamos que os testes sejam executados somente de a quantidade de falhas até o momento é menor do que N. Isto é útil para um cenário de produção em que um código só está pronto para implantação se passa pelo conjunto de testes com menos de N falhas.

Um exemplo de comando seria:

pytest test_compare.py -v --maxfail 1

Paralelização de testes

editar

Por padrão o pytest roda os testes de forma sequencial. Em um cenário real, o conjunto de testes poderia ser muito grande, e uma testagem de forma sequencial com funções longas seria muito lenta.

Para rodar os testes em paralelo temos que instalar as seguintes dependendias:

  • Para distribuilções Debian based:
pip install pytest-xdist
  • Para distribuições Arch Based:
sudo pacman -S python-pytest-xdist


O comando utilizado é:

pytest -n <num>

Onde num é a quantidade de testes que podem rodar em paralelo.

Podemos salvar os resultados dos testes em um arquivo XML com o seguinte comando:

pytest -v --junitxml="result.xml

Pytest na vida real

editar

Em situações reais, queremos poder testar funções de códigos grandes e complexos sem ter que reescrever as funções em um arquivo de teste. Para isto, basta importar o código contendo as funções desejadas. Vamos então criar dois arquivos, o primeiro arquivo nomeado como fatorial.py:

# Calcula o fatorial de x
def fatorial(x):
    int(x)
    fat=1
    
    for i in range(1,x+1):
        fat*=i
    
    return fat


E o segundo nomeado test_fatorial.py, que testará a função fatorial:

import pytest
from exemplo1 import fatorial


def test_fatorial():
    fat = fatorial(5)
    
    assert fat == 120

O mesmo pode ser feito para vários arquivos diferentes e funções.

Pycheck

editar

Como dito no na seção de Teste simples, o pytest executa os testes de uma mesma função até a primeira falha de um assert, o que pode ser indesejado, pois os testes posteriores à falha podem ser importantes.

Para isso existe o plugin pycheck, que permite com que os testes continuem a ser executados, mesmo com a falha de algum assert.

Instalação

editar

Para distribuições Arch Based utilizamos o comando:

sudo pamac install python-pytest-check

Já para instalar em distribuições Debian Based, precisamos ter uma dependência, o pip do Python, e posteriormente utilizar o seguinte comando do pip:

sudo apt install python3-pip -y
pip install pycheck

No nosso arquivo de testes podemos importar a função check da biblioteca pytest_check, como no código a seguir.

import pytest
from pytest_check import check
from exemplo1 import fatorial

def test_fatorial():
    fat = fatorial(5)
    
    with check:
        assert fatorial(2) == 5
    with check:
        assert fatorial(5) == 7
    with check:
        assert fatorial(5) == 120
    with check:
        assert fatorial(4) == 24

Obtemos a saída:

test_exemplo.py::test_fatorial FAILED                                                            [100%]

=============================================== FAILURES ===============================================
____________________________________________ test_fatorial _____________________________________________
FAILURE: assert 2 == 5
 +  where 2 = fatorial(2)
test_exemplo.py:8 in test_fatorial() -> with check:

FAILURE: assert 120 == 7
 +  where 120 = fatorial(5)
------------------------------------------------------------
Failed Checks: 2
======================================= short test summary info ========================================
FAILED test_exemplo.py::test_fatorial
========================================== 1 failed in 0.02s ===========================================

Podemos perceber que mesmo com uma falha, os testes continuaram rodando na função test_fatorial().

editar