Test Runner Python

pytest

É uma biblioteca para escrever e executar testes em Python. Com ela, você pode garantir que seu código está funcionando como esperado — e continuar funcionando mesmo após mudanças.

  1. 🔍 O pytest procura por arquivos de teste no diretório atual e subdiretórios.
    Por padrão, ele busca arquivos que:

    • Começam com test_ ou terminam com _test.py.

  2. 📦 Dentro desses arquivos, ele procura por funções que começam com test_.

  3. ▶️ Ele executa essas funções e verifica se todos os asserts passaram.

  4. ❌ Se algum assert falhar, ele mostra uma mensagem de erro dizendo o que deu errado e onde.

assert (traduzindo seria "Garantir que", "Verificar se", etc...) é uma palavra-chave do Python usada para verificar se uma condição é verdadeira durante a execução do código. Se a condição for falsa, ele gera um erro (AssertionError). Usa-se assert para verificar se uma função está retornando o que deveria.

Uma fixture é uma função especial usada no pytest para preparar algum dado, ambiente ou recurso antes de rodar os testes. São dados temporários, criados para preparar o ambiente de teste. Com ela evita-se repetição de código além de poder ser reutilizadas em vários testes e deixam os testes mais organizados. 

pytest carrega automaticamente qualquer fixture definida em um arquivo chamado **conftest.py**, desde que o arquivo conftest.py esteja na mesma pasta dos testes ou em uma pasta acima na hierarquia.

padrões de nomenclaturas para arquivos base de testes:
Nome do Arquivo Quando usar Estilo de teste
conftest.py Padrão no pytest para setup/teardown, fixtures, e configurações globais Pytest
base_test.py ✅ Muito comum em unittest ou Django TestCase como classe base comum Django / unittest
test_utils.py ou test_helpers.py Para funções auxiliares (como login, criação de objetos, etc.) Funções auxiliares

Cada teste que usa uma fixture recebe uma nova instância dela, como se ela fosse “resetada” do zero toda vez. Isso garante que:

  • ✅ Os testes não interferem uns nos outros.

  • ✅ O estado da fixture está sempre “limpinho”.

  • ✅ Você pode confiar que seu teste está rodando isolado.

Mock (do inglês “fingir”, “simular”) é uma ferramenta usada para simular objetos ou comportamentos em testes, sem precisar usar os objetos reais. Em vez de chamar uma função real (como acessar um banco, enviar um email ou fazer uma requisição HTTP), você finge que ela existe e retorna o que você quiser. Ex. caso esteja testando uma função que envia um email, não é nada bom enviar um email em todo teste feito, então coloca um mock no lugar dessa função e finge que ela funcionou.

Testes parametrizados permitem rodar o mesmo teste várias vezes com dados diferentes, sem precisar repetir o código. É como escrever um modelo de teste, e o pytest repete ele trocando só os valores que você passou.

Marcadores (ou marks) são etiquetas que você coloca nos testes para identificar, agrupar ou controlar como eles são executados.

Marcador O que faz
@pytest.mark.skip Pula o teste (ignora completamente).
@pytest.mark.xfail Espera que o teste falhe (útil pra testes incompletos).
@pytest.mark.parametrize Executa o mesmo teste com diferentes entradas.
@pytest.mark.custom Um marcador que você inventa (ex: @pytest.mark.login).

As configurações gerais dos testes estarão presentes no arquivo pytest.ini que deverá ser colocado na raiz do projeto.

pytest-django é uma ponte de integração. Ele Integra profundamente o pytest com o Django. Cria e gerencia bancos de dados temporários para testes (como o TestCase do Django faz). Lê o DJANGO_SETTINGS_MODULE e etc. Sem ele, pytest não consegue rodar testes Django direito.

Comandos pytest

Importante sempre utilizar uma venv.  Será criada uma pasta .pytest_cache. Se estiver em um projeto django, instale e use também o pytest-django.

Obs.: essas opções, como rP, podem ser colocadas em addopts na extensão do vscode.

bash
$ pip install pytest
$ pip install pytest-django # instala um testador para projetos django
$ pip install pytest-watch # para executar continuamente
$ pytest --version
$ pytest # executar automaticamente todos os testes do seu projeto que usam a biblioteca pytest
$ pytest -s # Parâmetro para mostrar prints no terminal
$ pytest -m login # roda apenas teste com marcadores login
$ pytest -m 'not login' # roda todos os testes menos os testes com marcadores login (windows com aspas duplas "")
$ pytest -rP # captura e exibe os prints dentro dos testes.
$ pytest -k test_nome # executa um único test
$ ptw # roda o watch para assistir alterações
$ ptw -- -k test_nome # executa em modo watch apenas o teste descrito
test_pytest.py
# --- EXEMPLO 1 --- #

def test_soma():
    assert sum([1, 2, 5]) == 8


# --- EXEMPLO 2 --- #

def is_positive(number):
    return number > 0

def test_is_positive():
    assert is_positive(5) == True
    assert is_positive(-2) == False

# --- EXEMPLO 3 --- #

def sub(a,b):
    return a - b

def lenght(lista):
    return len(lista)
# Testa o resultado de duas funções em uma unica função de teste
def test_sub_e_lenght():
    assert sub(5,2) == 3
    assert lenght([1,2,3,4,5]) == 5

Neste exemplo as funções estão misturada com os teste apenas a titulo de exemplo.

| Exemplo de testes Separados

functions.py
def is_positive(number):
    return number > 0

def sub(a,b):
    return a - b

def lenght(lista):
    return len(lista)

def validate_email(email):
    if "@" in email and '.' in email:
        return True

def somar_lista(valores):
    """Soma todos os valores de uma lista."""
    if not all(isinstance(i, (int, float)) for i in valores):
        raise ValueError("Todos os valores da lista devem ser inteiros ou floats")
    return sum(valores)
    
def encontrar_valor(dicionario, chave):
    """Encontra valor no dicionario com base na chave """
    if not isinstance(dicionario,dict):
        raise ValueError("O primeiro valor deve ser um dicionario")
    return dicionario.get(chave, None)
test_functions.py
from functions import is_positive, sub, lenght, validate_email

def test_is_positive():
    assert is_positive(5) == True
    assert is_positive(-2) == False

def test_sub_e_lenght():
    assert sub(5,2) == 3
    assert lenght([1,2,3,4,5]) == 5

def test_validate_email():
    assert validate_email("test@gmail.com") == True
test_functions_com_tratamento.py
from functions import somar_lista, encontrar_valor
import pytest

def test_somar_lista():
    assert somar_lista([1, 2, 3, 4, 5]) == 15 # esta com numeros inteiros
    assert somar_lista([5.5,4.5,10]) == 20.0 # testa misturando float e int
    assert somar_lista([]) == 0 # testa lista vazia

    # Faz o pytest esperar um erro de valor passando 'c' na lista
    with pytest.raises(ValueError):
        somar_lista([1, 2, 'c']) # testa lista com strings

def test_encontrar_valor():
    dicionario = {'a': 1, 'b': 2, 'c': 3}
    assert encontrar_valor(dicionario, 'a') == 1
    assert encontrar_valor(dicionario, 'b') == 2
    assert encontrar_valor(dicionario, 'c') == 3
    assert encontrar_valor(dicionario, 'd') is None # testa valor não existente
    
    with pytest.raises(ValueError):
        encontrar_valor(['a','b','c'], 'a') # testa valor não existente com valor invalido

Esse arquivo testa a lista e o dicionario de functions.py, e trata erros.
 

A construção with pytest.raises(ValueError): diz para o pytest:

"Eu espero que dentro desse bloco de código aconteça um erro do tipo ValueError. Se isso acontecer, o teste PASSA. Se não acontecer, o teste FALHA."

| Exemplos fixtures

test_sem_fixture.py
def test_soma1():
    numeros = [1, 2, 3]
    assert sum(numeros) == 6

def test_soma2():
    numeros = [1, 2, 3]
    assert len(numeros) == 3

Neste exemplo, a lista numeros é repetida em cada teste. E se você quisesse mudar os números? Teria que mudar em todos os testes manualmente.

test_com_fixture.py
import pytest

# Criando a fixture
@pytest.fixture
def numeros():
    return [1, 2, 3]

# Usando a fixture no teste
def test_soma(numeros):
    assert sum(numeros) == 6

def test_quantidade(numeros):
    assert len(numeros) == 3

No codigo abaixo: 

  • @pytest.fixture marca a função numeros() como uma fixture.

  • Quando você escreve def test_soma(numeros):, o pytest vê que o teste precisa da fixture numeros.

  • Ele executa a fixture, pega o retorno ([1, 2, 3]) e entrega como argumento para o teste.

  • Agora seu teste já começa com os dados prontos, sem repetição de código.

| Fixture com Banco de dados

fixture_db_simulado.py
import pytest

# 🔧 Fixture que retorna um "banco de dados" novo a cada teste
@pytest.fixture
def banco_simulado():
    print("🔁 Criando novo banco de dados...")
    return {}

# 🧪 Teste 1: insere um dado
def test_inserir_usuario(banco_simulado):
    banco_simulado['usuario'] = 'Diogo'
    assert banco_simulado['usuario'] == 'Diogo'

# 🧪 Teste 2: banco está limpo novamente
def test_banco_esta_vazio(banco_simulado):
    assert 'usuario' not in banco_simulado

 Cada teste ganha seu próprio dicionário limpo. Um teste pode alterar os dados, mas isso não afeta os outros.

 

fixture_db_sqlite.py
import pytest
import sqlite3

# 🎯 Fixture que cria um banco SQLite em memória e prepara a tabela
@pytest.fixture
def banco():
    print("🔁 Criando banco em memória...")
    conexao = sqlite3.connect(":memory:")  # Banco temporário na RAM
    cursor = conexao.cursor()
    cursor.execute("CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT)")
    conexao.commit()
    yield conexao  # Entrega o banco para o teste
    conexao.close()  # Fecha após o teste

# 🧪 Teste de inserção de dados
def test_inserir_usuario(banco):
    banco.execute("INSERT INTO usuarios (nome) VALUES (?)", ("Diogo",))
    banco.commit()

    resultado = banco.execute("SELECT nome FROM usuarios").fetchone()
    assert resultado[0] == "Diogo"

# 🧪 Teste que garante que o banco está limpo
def test_banco_vazio(banco):
    resultado = banco.execute("SELECT * FROM usuarios").fetchall()
    assert resultado == []

Tudo que vier após o yield será executado depois que o teste terminar, como a limpeza do banco. Quando usamos yield, o código pausa ali, entrega a conexão pro teste usar, e quando o teste termina, o pytest volta e executa o que vem depois do yield — ou seja, a limpeza! Se fosse com o return o programa se encerraria ali e não fecharia o banco na linha de baixo.

OBS.: o pytest chama automaticamente a função da fixture e usa next() nos bastidores.

mock.py
import pytest
from unittest.mock import MagickMock

@pytest.fixture
def mock_response():
    """Fixture que retorna um objeto de resposta mockado com status_code 200 e conteúdo JSON."""
    # cria um objeto simulado (mock) chamado response.
    response = MagickMock()
    # define o status_code do objeto simulado como 200.
    response.status_code = 200
    # define o conteúdo do objeto simulado como um dicionário JSON.
    response.json.return_value = {'message': 'success'}
    return response

def test_usa_mock_response(mock_response):
    assert mock_response.status_code == 200
    assert mock_response.json() == {'message': 'success'}

Esse objeto (response) pode ter qualquer atributo ou método que você imaginar, mesmo que ele não exista na classe original. Por isso o nome "mágico" — ele se adapta a tudo.

test_parametrizado.py
import pytest

def somar(a, b):
    return a + b

@pytest.mark.parametrize("a, b, esperado", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (10, 5, 15)
])
def test_somar(a, b, esperado):
    assert somar(a, b) == esperado

O pytest vai executar o mesmo teste test_somar 4 vezes, trocando os valores de a, b e esperado a cada rodada. O @pytest.mark.parametrize é um decorador, então ele deve ficar logo acima da função de teste que você quer parametrizar.

| Marcadores Exemplo

mark.py
import pytest

@pytest.mark.login
def test_login_valido():
    assert True

@pytest.mark.slow
def test_processo_demorado():
    assert True
pytest.ini
[pytest]
markers =
    login: marca testes relacionados a login
    slow: marca testes lentos
    api: marca testes da API

Quando você cria marcadores personalizados, como @pytest.mark.login, @pytest.mark.slow, etc., o pytest exige que você os registre em um arquivo de configuração chamado pytest.ini. Isso evita erros de digitação e ajuda a documentar os marcadores que o projeto está usando.

| Exemplo completo

Esse exemplo mostra um teste usando todas as melhores boas praticas e algumas das principais tecnicas.

meu_projeto/

├── src/
│   └── carrinho.py

├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_adicao.py
│   ├── test_remocao.py
│   └── test_email.py

├── pytest.ini
└── requirements.txt

 

carrinho.py
class CarrinhoDeCompras:
    def __init__(self):
        self.itens = {}

    def adicionar(self, produto, quantidade):
        if quantidade <= 0:
            raise ValueError("Quantidade deve ser positiva")
        self.itens[produto] = self.itens.get(produto, 0) + quantidade

    def remover(self, produto):
        if produto not in self.itens:
            raise ValueError("Produto não encontrado")
        del self.itens[produto]

    def total(self):
        return sum(self.itens.values())
conftest.py
import pytest
from src.carrinho import CarrinhoDeCompras

@pytest.fixture
def carrinho_vazio():
    return CarrinhoDeCompras()
test_adicao.py
import pytest

@pytest.mark.adicao
def test_adicionar_produtos(carrinho_vazio):
    carrinho_vazio.adicionar("Shampoo", 2)
    carrinho_vazio.adicionar("Sabonete", 3)
    assert carrinho_vazio.total() == 5

@pytest.mark.adicao
def test_adicionar_quantidade_invalida(carrinho_vazio):
    with pytest.raises(ValueError):
        carrinho_vazio.adicionar("Shampoo", 0)
test_remocao.py
import pytest

@pytest.mark.remocao
def test_remover_produto_existente(carrinho_vazio):
    carrinho_vazio.adicionar("Sabonete", 1)
    carrinho_vazio.remover("Sabonete")
    assert carrinho_vazio.total() == 0

@pytest.mark.remocao
def test_remover_produto_inexistente(carrinho_vazio):
    with pytest.raises(ValueError):
        carrinho_vazio.remover("Shampoo")
test_email.py
import pytest
from unittest.mock import MagicMock

class EmailService:
    def enviar_email(self, destinatario, titulo, corpo, servico_envio):
        return servico_envio.enviar(destinatario, titulo, corpo)

@pytest.fixture
def email_service():
    return EmailService()

@pytest.fixture
def email_mock():
    mock = MagicMock()
    mock.enviar.return_value = True
    return mock

@pytest.mark.email
def test_envio_sucesso(email_service, email_mock):
    assert email_service.enviar_email("diogo@email.com", "Oi", "Mensagem", email_mock)

@pytest.mark.email
def test_envio_falha(email_service, email_mock):
    email_mock.enviar.return_value = False
    assert not email_service.enviar_email("diogo@email.com", "Oi", "Mensagem", email_mock)
pytest.ini
[pytest]
minversion = 6.0 # compatibilidade da versão minima
addopts = -ra -q
testpaths = tests
markers =
    adicao: testes relacionados à adição de produtos
    remocao: testes de remoção de produtos
    email: testes de envio de email

-r: mostra o resumo dos testes no final (a = all, pode ser f, E, x, etc.).

-q: modo quieto, ou seja, menos saída de log — apenas o essencial.

testpaths = tests: informa o pytest para procurar os testes apenas na pasta tests/.