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
editarO 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
editarPara 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
editarRodar 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
editarPodemos 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
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
editarFixtures 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
editarO 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:
editarA 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:
editarXFAIL
editarO 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
editarAs 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
editarPodemos 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
editarPor 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
editarPodemos 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
editarComo 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
editarPara 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
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()
.