Pytest + check
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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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:
- Nome do arquivo;
- Indicação de quais testes deram errado e quais deram certo.
F
significa que a função falhou;.
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 | editar código-fonte]
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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]XFAIL
[editar | editar código-fonte]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.
SKIP
[editar | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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.
XML
[editar | editar código-fonte]Podemos salvar os resultados dos testes em um arquivo XML com o seguinte comando:
pytest -v --junitxml="result.xml
Pytest na vida real
[editar | editar código-fonte]
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 | editar código-fonte]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 | editar código-fonte]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
Uso
[editar | editar código-fonte]
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()
.