<h2>Introdução ao pytest: Por Que Abandonar print() nos Testes</h2>
<p>Quando comecei a programar, assim como muitos, testava meu código inserindo <code>print()</code> em diversos pontos e verificando manualmente a saída. Isso funciona para scripts pequenos, mas quando seu projeto cresce, essa abordagem se torna caótica e improdutiva. O pytest é um framework que transforma testes em uma prática sistemática, legível e automatizável — permitindo que você execute centenas de testes em segundos e saiba exatamente o que quebrou.</p>
<p>O pytest se diferencia de outras ferramentas porque é minimalista: você escreve funções Python normais com nomes começados em <code>test_</code>, e ele as encontra e executa automaticamente. Não há necessidade de herdar classes, usar decoradores complicados ou aprender uma sintaxe própria. Vamos entender como começar e, progressivamente, construir uma estratégia sólida de testes.</p>
<h2>Fundamentos: Escrevendo Seu Primeiro Teste</h2>
<h3>Instalação e Primeiro Teste</h3>
<p>A instalação é simples — use pip para adicionar pytest ao seu projeto:</p>
<pre><code class="language-bash">pip install pytest</code></pre>
<p>Agora, crie um arquivo chamado <code>test_calculadora.py</code>:</p>
<pre><code class="language-python">def soma(a, b):
return a + b
def subtrai(a, b):
return a - b
def test_soma_positivos():
assert soma(2, 3) == 5
def test_soma_negativos():
assert soma(-1, -2) == -3
def test_subtrai():
assert subtrai(10, 5) == 5</code></pre>
<p>Execute com <code>pytest test_calculadora.py</code> ou simplesmente <code>pytest</code> (pytest procura automaticamente por arquivos e funções que começam com <code>test_</code>). A saída será clara: quantos testes passaram, quantos falharam e por quê.</p>
<h3>Entendendo assert e Mensagens de Erro</h3>
<p>O <code>assert</code> é a coluna vertebral dos testes em pytest. Você escreve uma condição e, se for falsa, o teste falha. Mas pytest é inteligente: ele mostra exatamente o que diferiu. Veja um exemplo real:</p>
<pre><code class="language-python">def calcula_desconto(preco, percentual):
return preco * (1 - percentual / 100)
def test_desconto_inválido():
resultado = calcula_desconto(100, 10)
assert resultado == 91.0, f"Esperado 90.0, mas obtive {resultado}"</code></pre>
<p>Se colocar uma lógica errada na função, a mensagem será clara. Pytest mostra o valor real vs. esperado automaticamente, tornando debug muito mais rápido que <code>print()</code> tradicional.</p>
<h3>Organizando Testes em Diretórios</h3>
<p>Para projetos maiores, organize seus testes em uma estrutura clara:</p>
<pre><code>meu_projeto/
├── src/
│ ├── calculadora.py
│ ├── validador.py
│ └── __init__.py
├── tests/
│ ├── test_calculadora.py
│ ├── test_validador.py
│ └── conftest.py
├── pytest.ini
└── requirements.txt</code></pre>
<p>Crie um arquivo <code>pytest.ini</code> na raiz do projeto para configurar o comportamento padrão:</p>
<pre><code class="language-ini">[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short</code></pre>
<p>Isso garante que pytest sempre procure em <code>tests/</code> e exiba saída verbosa com rastreamento de erro reduzido.</p>
<h2>Fixtures: Reutilizando Recursos e Dados Entre Testes</h2>
<h3>O Conceito de Fixture e Por Que Usar</h3>
<p>Uma fixture é uma função que prepara dados ou recursos que seus testes precisam. Imagine que você testa uma classe <code>UsuarioService</code> que precisa conectar a um banco de dados. Sem fixtures, você escreveria o setup em cada teste — código duplicado. Com fixtures, você define uma única vez e pytest a executa automaticamente para cada teste que a solicita.</p>
<p>Fixtures resolvem três problemas: <strong>eliminar duplicação</strong>, <strong>garantir limpeza de recursos</strong> (como fechar conexões) e <strong>facilitar testes que dependem do mesmo estado inicial</strong>.</p>
<h3>Criando e Usando Fixtures Básicas</h3>
<p>Crie um arquivo <code>tests/conftest.py</code> — esse é o arquivo especial onde pytest procura por fixtures:</p>
<pre><code class="language-python">import pytest
@pytest.fixture
def usuario_padrao():
return {
"id": 1,
"nome": "João Silva",
"email": "joao@example.com",
"ativo": True
}
@pytest.fixture
def lista_usuarios():
return [
{"id": 1, "nome": "João", "email": "joao@example.com"},
{"id": 2, "nome": "Maria", "email": "maria@example.com"},
{"id": 3, "nome": "Pedro", "email": "pedro@example.com"},
]</code></pre>
<p>Agora, em qualquer teste, você solicita a fixture como argumento:</p>
<pre><code class="language-python">def test_usuario_tem_email(usuario_padrao):
assert usuario_padrao["email"] == "joao@example.com"
def test_usuario_ativo(usuario_padrao):
assert usuario_padrao["ativo"] is True
def test_contar_usuarios(lista_usuarios):
assert len(lista_usuarios) == 3</code></pre>
<p>Pytest injeta as fixtures automaticamente. Se você precisar usar <code>usuario_padrao</code> em 20 testes, basta adicioná-la como argumento em cada um — zero duplicação.</p>
<h3>Fixtures com Setup e Teardown</h3>
<p>Frequentemente você precisa preparar algo antes do teste e limpar depois. Use <code>yield</code> para isso:</p>
<pre><code class="language-python">import sqlite3
import pytest
@pytest.fixture
def db_conexao():
Setup: criar conexão
conexao = sqlite3.connect(":memory:")
cursor = conexao.cursor()
cursor.execute("""
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
email TEXT UNIQUE
)
""")
conexao.commit()
O teste roda aqui
yield conexao
Teardown: fechar conexão
conexao.close()
def test_inserir_usuario(db_conexao):
cursor = db_conexao.cursor()
cursor.execute("INSERT INTO usuarios (nome, email) VALUES (?, ?)",
("João", "joao@example.com"))
db_conexao.commit()
cursor.execute("SELECT * FROM usuarios WHERE email = ?",
("joao@example.com",))
resultado = cursor.fetchone()
assert resultado is not None
assert resultado[1] == "João"</code></pre>
<p>O <code>yield</code> marca o ponto onde o teste roda. Código antes do <code>yield</code> é setup, código depois é teardown. Isso garante que a conexão sempre seja fechada, mesmo se o teste falhar.</p>
<h3>Escopos de Fixture: Quando Reutilizar</h3>
<p>Fixtures têm escopos que determinam quantas vezes são criadas. O padrão é <code>function</code> (uma por teste), mas existem outras opções:</p>
<pre><code class="language-python">@pytest.fixture(scope="function") # Padrão: nova instância por teste
def recurso_por_funcao():
return {"valor": "novo"}
@pytest.fixture(scope="module") # Uma instância para todos os testes do módulo
def recurso_modulo():
return sqlite3.connect(":memory:")
@pytest.fixture(scope="session") # Uma instância para toda a sessão de testes
def recurso_sessao():
return {"config": "global"}
def test_um(recurso_por_funcao):
recurso_por_funcao["valor"] = "modificado"
def test_dois(recurso_por_funcao):
Aqui recurso_por_funcao é uma nova instância, não afetada pelo test_um
assert recurso_por_funcao["valor"] == "novo"</code></pre>
<p>Use <code>module</code> ou <code>session</code> com cuidado — testes podem se afetar mutuamente se compartilharem estado. Geralmente, <code>function</code> é a escolha segura.</p>
<h3>Fixtures Parametrizadas</h3>
<p>Às vezes você quer executar o mesmo teste com dados diferentes. Use <code>params</code>:</p>
<pre><code class="language-python">@pytest.fixture(params=[
{"entrada": 2, "esperado": 4},
{"entrada": 3, "esperado": 9},
{"entrada": -1, "esperado": 1},
])
def casos_potencia(request):
return request.param
def quadrado(n):
return n ** 2
def test_potencia(casos_potencia):
assert quadrado(casos_potencia["entrada"]) == casos_potencia["esperado"]</code></pre>
<p>Pytest rodará <code>test_potencia</code> três vezes — uma para cada item em <code>params</code>. Você obtém 3 testes por 1 função de teste, reduzindo duplicação massivamente.</p>
<h2>Organização Profissional de Testes: Estrutura e Boas Práticas</h2>
<h3>Estrutura de Diretórios Escalável</h3>
<p>Conforme seu projeto cresce, organize testes por módulos de funcionalidade:</p>
<pre><code>projeto/
├── src/
│ ├── usuarios/
│ │ ├── models.py
│ │ ├── services.py
│ │ └── __init__.py
│ ├── pedidos/
│ │ ├── models.py
│ │ ├── services.py
│ │ └── __init__.py
│ └── __init__.py
├── tests/
│ ├── conftest.py # Fixtures globais
│ ├── test_usuarios/
│ │ ├── conftest.py # Fixtures específicas de usuarios
│ │ ├── test_models.py
│ │ └── test_services.py
│ └── test_pedidos/
│ ├── conftest.py
│ ├── test_models.py
│ └── test_services.py
└── pytest.ini</code></pre>
<p>Pytest procura por <code>conftest.py</code> em cada nível de diretório, permitindo fixtures globais e específicas por módulo. Isso mantém tudo organizado e evita que você carregue fixtures desnecessárias.</p>
<h3>Estrutura de Testes para Classes</h3>
<p>Para código orientado a objetos, organize testes em classes:</p>
<pre><code class="language-python">class Usuario:
def __init__(self, nome, email):
self.nome = nome
self.email = email
self.ativo = True
def desativar(self):
self.ativo = False
def validar_email(self):
return "@" in self.email
class TestUsuario:
@pytest.fixture
def usuario(self):
return Usuario("João", "joao@example.com")
def test_criacao(self, usuario):
assert usuario.nome == "João"
def test_desativar(self, usuario):
usuario.desativar()
assert usuario.ativo is False
def test_validar_email_valido(self, usuario):
assert usuario.validar_email() is True
def test_validar_email_invalido(self):
usuario_invalido = Usuario("Maria", "maria_sem_email")
assert usuario_invalido.validar_email() is False</code></pre>
<p>Classes agrupam testes relacionados, facilitando leitura e manutenção. Pytest as trata normalmente — não é necessário herdar de nada.</p>
<h3>Marcadores (Markers) para Categorizar Testes</h3>
<p>Use marcadores para executar subconjuntos de testes:</p>
<pre><code class="language-python">import pytest
@pytest.mark.rapido
def test_soma():
assert 1 + 1 == 2
@pytest.mark.lento
def test_leitura_arquivo_grande():
Simula teste que demora
import time
time.sleep(2)
assert True
@pytest.mark.integracao
def test_conectar_banco_dados():
Simula conexão real
assert True</code></pre>
<p>No terminal, execute apenas testes rápidos com <code>pytest -m rapido</code> ou evite os lentos com <code>pytest -m "not lento"</code>. Registre marcadores no <code>pytest.ini</code>:</p>
<pre><code class="language-ini">[pytest]
markers =
rapido: testes que executam em menos de 1 segundo
lento: testes que demoram mais de 1 segundo
integracao: testes que acessam banco de dados ou APIs externas</code></pre>
<h3>Testando Exceções e Comportamentos Esperados</h3>
<p>Às vezes você quer verificar se uma exceção é lançada:</p>
<pre><code class="language-python">import pytest
def dividir(a, b):
if b == 0:
raise ValueError("Divisão por zero não permitida")
return a / b
def test_divisao_por_zero():
with pytest.raises(ValueError, match="Divisão por zero"):
dividir(10, 0)
def test_divisao_valida():
assert dividir(10, 2) == 5.0</code></pre>
<p><code>pytest.raises()</code> garante que a exceção é lançada. O parâmetro <code>match</code> verifica se a mensagem contém o padrão esperado. Isso testa comportamento defensivo — código que reage bem a entradas inválidas.</p>
<h3>Mocking: Testando sem Dependências Externas</h3>
<p>Frequentemente você testa código que depende de APIs, bancos de dados ou serviços externos. Use <code>unittest.mock</code> para simular essas dependências:</p>
<pre><code class="language-python">from unittest.mock import Mock, patch
import pytest
class PedidoService:
def __init__(self, db):
self.db = db
def criar_pedido(self, usuario_id, itens):
Simula criação de pedido
pedido = {"usuario_id": usuario_id, "itens": itens}
self.db.salvar(pedido)
return pedido
def test_criar_pedido_com_mock():
Cria um mock (objeto falso) do banco de dados
db_mock = Mock()
service = PedidoService(db_mock)
resultado = service.criar_pedido(1, ["item1", "item2"])
Verifica se o método foi chamado com os argumentos corretos
db_mock.salvar.assert_called_once()
args = db_mock.salvar.call_args[0][0]
assert args["usuario_id"] == 1
assert len(args["itens"]) == 2</code></pre>
<p>Mocks permitem testar lógica sem depender de infraestrutura real, tornando testes mais rápidos e confiáveis.</p>
<h2>Configuração Avançada e Boas Práticas</h2>
<h3>Arquivo pytest.ini Completo</h3>
<p>Configure pytest para comportar-se como você espera:</p>
<pre><code class="language-ini">[pytest]
testpaths = tests
python_files = test_.py _test.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
-ra
markers =
rapido: testes de execução rápida
lento: testes que demoram mais de 1 segundo
integracao: testes que acessam recursos externos
unitario: testes unitários isolados
filterwarnings =
ignore::DeprecationWarning</code></pre>
<p><code>-v</code> mostra cada teste, <code>--tb=short</code> reduz mensagens de erro verbosas, <code>-ra</code> resumo de tudo (skipped, xfailed, etc).</p>
<h3>Cobertura de Testes com pytest-cov</h3>
<p>Saiba quanto do seu código está sendo testado:</p>
<pre><code class="language-bash">pip install pytest-cov
pytest --cov=src --cov-report=html</code></pre>
<p>Isso gera um relatório HTML mostrando linhas cobertas e não cobertas. Não é sobre alcançar 100% — é sobre testar o código crítico e conhecer seus gaps.</p>
<h3>Testes Parametrizados com pytest.mark.parametrize</h3>
<p>Para múltiplas combinações de entrada, use <code>parametrize</code>:</p>
<pre><code class="language-python">import pytest
@pytest.mark.parametrize("entrada,esperado", [
(2, 4),
(3, 9),
(-1, 1),
(0, 0),
])
def test_quadrado(entrada, esperado):
def quadrado(n):
return n ** 2
assert quadrado(entrada) == esperado</code></pre>
<p>Pytest rodará o teste 4 vezes, uma para cada tupla. A sintaxe é simples: nomes das variáveis e lista de valores.</p>
<h2>Conclusão</h2>
<p>Depois de caminhar por esses conceitos, retenha três pontos essenciais: <strong>Primeiro, fixtures são o coração do pytest — use-as para eliminar duplicação e gerenciar recursos com setup/teardown automático.</strong> Elas transformam testes de algo repetitivo em algo elegante e mantenível. <strong>Segundo, organize seus testes em uma estrutura clara com diretórios, conftest.py e marcadores — isso permite executar subconjuntos de testes rapidamente e mantém projetos escaláveis.</strong> Um teste deveria responder: "o que estou testando e por quê?" em sua estrutura de arquivo. <strong>Terceiro, use marcadores, mocking e parametrização para cobrir casos reais sem depender de infraestrutura externa — testes rápidos e confiáveis são testes que serão executados frequentemente.</strong></p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.pytest.org/" target="_blank" rel="noopener noreferrer">Documentação Oficial do pytest</a></li>
<li><a href="https://docs.pytest.org/en/stable/fixture.html" target="_blank" rel="noopener noreferrer">pytest Fixtures - Guia Completo</a></li>
<li><a href="https://realpython.com/pytest-python-testing/" target="_blank" rel="noopener noreferrer">Real Python: pytest Tutorial</a></li>
<li><a href="https://docs.pytest.org/en/latest/goodpractices.html" target="_blank" rel="noopener noreferrer">pytest Best Practices</a></li>
<li><a href="https://docs.python.org/3/library/unittest.mock.html" target="_blank" rel="noopener noreferrer">unittest.mock Documentation</a></li>
</ul>
<p><!-- FIM --></p>