Tutorial de pytest para iniciantes

Leonardo Galani
assert(QA)
Published in
7 min readJul 28, 2020
https://pixnio.com/free-images/2018/11/19/2018-11-19-17-59-10.jpg

TLDR: Se você manja um pouco de inglês ou gostaria de saber todas as possibilidades, você pode simplesmente ir para documentação oficial do pytest https://docs.pytest.org/en/stable/index.html e usar o tradutor do google.

Se você está começando com python e ainda não conhece essa maravilhosa ferramenta de teste, nesse tutorial inicial vou abordar o uso básico e deixar claro algumas convenções e melhores práticas para você começar do jeito certo.

Existe uma possibilidade grande desse post ser a primeira parte de uma série de posts sobre pytest, mas isso vai depender do público e se Maria Clara vai topar continuar esses posts comigo :D

Mas o que é o pytest?

O pytest é uma framework 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.

É o queridinho da comunidade por sua flexibilidade, pela forma que usa fixtures e pela facilidade de estender suas funcionalidades.

Por mais que seja super popular, é curioso que não tenha muitos posts em português falando sobre essa belezinha, então vamos começar a fazer uma série de posts de como tirar proveito desta obra de arte.

Para instalar é tão simples quanto um

pip install pytest

Entendendo como pytest funciona

Antes de sair mudando toda sua code base para usar o brinquedo novo, vamos entender como o pytest funciona.

Quando você executa o comando pytest dentro do seu ambiente virtual python, ele vai fazer um scan nos diretórios e subdiretórios do seu repositório procurando por arquivos que respeitem o formato de nomenclatura test_*.py ou *_test.py.

Como o processo de descoberta é amplo e existe mais de uma forma de chamar o pytest para rodar (tanto comando direto pytest mencionado anteriormente, quanto com o comando python -m pytest), é altamente recomendado usar o padrão__init__.py em cada diretório para o pytest reconhecê-los como módulos e evitar colisão de nome nos arquivo de teste.

Também se aconselha o uso dos padrões de organização de código-fonte caso seus testes estejam no mesmo repositório do código testado. A documentação oficial do pytest dá algumas sugestões de como organizar seus módulos de teste e módulos da aplicação:

setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
# retirado de https://docs.pytest.org/en/stable/goodpractices.html#test-discovery

Dentro do arquivo que contém o seu teste, também é preciso respeitar a nomenclatura das classes e dos métodos de teste para que o pytest reconheça o que é preciso executar.

Exemplo 1:

def test_foo_bar():
assert True

def not_foo_bar():
pass

Levando em consideração o exemplo acima, o método test_foo_bar será executado, porém, o método not_foo_bar será deixado de lado.

Exemplo 2:

class TestFOOBAR():
def bar_nao_executado():
pass
def test_another_foo_bar():
assert True

class SeiLa():
def test_foo_bar_return():
assert True

Neste segundo exemplo, a classe SeiLa tem um método que está nos padrões de nomenclatura test_foo_bar_return porém, não segue o padrão de nomenclatura de classe que o pytest procura. Se você quer agrupar seus testes em uma classe, use o padrão Test* — como é possível ver no exemplo TestFOOBAR em conjunto com o padrão de nomenclatura para métodos de teste.

Agrupando e Executando testes

Existem diversas formas agrupar, porém, como esse é um tutorial básico e introdutório, vamos ficar com os exemplos mais simples.

Você já sabe que se executar o comando pytest ele irá fazer aquela busca nos diretórios e executar os métodos e classes que atendem ao padrão do pytest, contudo, existem outras formas de chamar seus testes.

Vou separar em exemplos para poder explicar um pouco melhor.

Exemplo 1:

Leve em consideração a seguinte estrutura de um projeto de teste em python:

tests/
__init__.py
user/
__init__.py
test_new_user.py
store/
__init__.py
test_new_store.py
test_delete_store.py
some_helper.py

Suponhamos que você precise rodar somente os testes dentro do diretório store. Para isso, podemos executar o pytest da seguinte forma:

pytest tests/store/

Neste caso, os 2 arquivos com nome test serão executados, deixando de lado todos os outros.
Seguindo a mesma ideia, para rodar somente um arquivo, você pode executar da seguinte forma:

pytest tests/store/test_new_store.py

Agora imagine que você está debugando um teste e não quer executar todos métodos de teste de um arquivo, para isso você pode executar o seguinte comando:

pytest tests/store/test_new_store.py::test_metodo_de_teste

Repare que o argumento::test_metodo_de_teste foi adicionado no comando, onde test_metodo_de_teste é o nome do método específico dentro do arquivo test_new_store.py.

Exemplo 2 :

Levando em consideração a estrutura do exemplo acima, digamos que você quer executar testes que estão espalhados em diversos arquivos, mas você não quer executar todos os métodos de testes, pois você gostaria de criar uma suite de teste de sanidade ou "smoke tests".

Para realizar esse tipo de agrupamento, precisamos usar uma funcionalidade do pytest chamada marks que é, basicamente, uma anotação de metadados de uma classe ou um método.

Com pytest.marks é possível marcar um teste para não ser executado (skip), definir parametro de execução data-driven (parametrize) e/ou definir um metadado "customizado".
Esse metadado customizado é o que iremos utilizar para fazer a tag dos métodos e tags de teste.

Exemplo 1:

import pytest


@pytest.mark.smoke
def test_send_http():
assert True
# Exemplo baseado na documentação
https://docs.pytest.org/en/stable/example/markers.html#mark-examples

Neste exemplo estamos dizendo que o método de teste test_send_http está "marcado" como smoke, porém, se você tentar executar sem registrar esse mark customizado, ele vai reclamar que não sabe o que 'smoke' significa.

Para isso você precisa criar um arquivo chamado pytest.ini na raiz do seu repositório de teste.

O conteúdo pode ser algo assim:

[pytest]
addopts
= --strict-markers
markers
=
smoke

Neste exemplo de arquivo pytest.ini também estou incluindo o argumento --strict-makers para dizer o pytest não aceitar nenhum marker que não esteja explícito neste documento, evitando problemas com digitação errada.

Exemplo 2:

Imagine que você tem uma classe com uns 5~10 métodos de teste e todos são do mesmo grupo de execução. Você não precisa colocar uma anotação em cada método, você pode, simplesmente, colocar a anotação na classe ou como um atributo da classe:

import pytestclass TestNewUser:
pytestmark = pytest.mark.slow

def test_new_user_with_something(self):
assert True

@pytest.mark.smoke
class
TestNewStore:

def test_new_store_with_something(self):
assert True

Para a classe TestNewUser uma constante chamada pytestmark foi definida com pytest.mark.slow e com essa informação o pytest irá aplicar a marcação para todos os métodos da classe.

O mesmo aconteceu para classe TestNewStore porém, ao invés de usar uma constante especifica do pytest, a anotação é feita a nível da classe e não dos métodos.

Rodando os testes agrupados:

Para executar seus testes com marks especificos, você pode rodar da seguinte forma:

pytest -m slow

Pytest Fixtures

Uma das grandes barreiras para pessoas que caem de paraquedas no pytest é entender como executar processos antes e depois do teste.

Quem vem do rspec (ou similares) sestá acostumado com hooks tipobeforeAll e afterMethod mas a abordagem do pytest para fazer setup e teardown (antes e depois do teste) é diferente.

Vamos dar uma olhada em alguns exemplos.

Exemplo 1:

import pytest


@pytest.fixture
def smtp_connection():
import smtplib

return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

@pytest.fixture

def create_user():
return {'name': 'Novo Usuário'}
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
#Exemplo baseado da documentação oficial https://docs.pytest.org/en/stable/fixture.html

Neste exemplo temos um teste chamado test_ehlo que recebe um argumento chamado smtp_connection e esse argumento é o nome de um método anotado como uma fixture .

Isso quer dizer que o método smtp_connection será executado antes do método de teste test_ehlo e é possível utilizar o objeto retornado pela fixture.
Vale também resaltar que a fixture create_user não será executada pois ela não foi invocada.

Exemplo 2:

import pytest
@pytest.fixture
def define_target():
return {'url': 'smtp.gmail.com', 'port': 587}
@pytest.fixture
def smtp_connection(define_target):
import smtplib
url = define_target['url']
port = define_target['port']
return smtplib.SMTP(url, port, timeout=5)def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
#Exemplo baseado da documentação oficial https://docs.pytest.org/en/stable/fixture.html

Outra capacidade das fixtures do pytest é possibilidade de "empilhar" fixtures e fazer o teste chamar somente a última.

Note que a fixure smtp_connection recebe a fixture define_target como argumento e usa ele para definir os parametros da chamada smtplib.SMTP.

Também leve em consideração que as fixtures estão no mesmo arquivo de teste e isso pode ser um problema quando existe uma grande quantidade de teste cases e suites.
Esse problema aumenta quando fixtures são empilhadas e o pytest obriga você a fazer o import de todas se elas estiverem em outro arquivo. Esse problema é resolvido em partes quando utilizado o plugin* conftest (vem junto com o pytest mas é considerado plugin) que será abordado mais a frente.

Exemplo 3:

import pytestdef create_user():
return {'user': 'foobar'}
def delete_user(user):
...
@pytest.fixture
def user_setup():
user = create_user()
yield user
delete_user(user)
def test_new_user(user_setup):
assert user_setup['user'] == 'foobar'

No exemplo acima é utilizado a função yield que pode ser aplicada para realizar setups e teardowns.
Note que o objeto user não está no return da fixture, mas sim como um parametro da função yield. Isso quer dizer que a fixture será executada, o metodo de teste será chamado com o argumento user e, depois que o teste se finaliza, a fixture continua sua execução e invoca o método delete_user .

Exemplo 4 (conftest):

Para evitar o "import hell" que pode acontecer ao se ter muitas fixtures que são empilhadas, é uma boa prática utilizar o plugin conftest como mencionado anteriormente.
Não é preciso fazer nenhuma instalação adicional pois é um plugin que já vem com o pytest por padrão.

Para entender melhor como ele funciona, vamos levar em consideração a estrutura abaixo:

tests/
__init__.py
conftest.py
user/
__init__.py
test_ne.py
store/
__init__.py
test_new_store.py
test_delete_store.py
some_helper.py

Veja que o arquivo conftest está na raiz dos testes e você não precisa fazer import de nenhuma fixture que esteja nesse arquivo. É só adicionar o nome da fixture como argumento do seu teste e pronto.

As fixtures deste arquivo serão disponibilizadas para todos os subdiretórios, e você pode ter múltiplos arquivos conftest em lugares estratégicos do seu repositório, respeitando a regra de disponibilização apenas para diretórios filhos e não diretórios pais.

Bonus Trick:

Quando eu comecei a programar eu era um daqueles que colocava print de debug aleatórios para fazer tracking do meu código e se desse algum problema, printar as coisas antes do exception. Depois eu conheci os debuggers e minha vida mudou. Agora imagina a minha felicidade quando descobri o pytest por padrão já vem com pdb hookado por linha de comando caso algum teste seu falhe! Para isso basta executar o pytest com o argumento --pdb :

pytest -m slow --pdb

Críticas e sugestões são bem vindas!
Se esse post não floopar a gente faz uma série mais completa!!
Aguardamos seu feedback!

--

--