Python

Como Usar pytest em Python: Fundamentos, Fixtures e Organização de Testes em Produção

17 min de leitura

Como Usar pytest em Python: Fundamentos, Fixtures e Organização de Testes em Produção

Introdução ao pytest: Por Que Abandonar print() nos Testes Quando comecei a programar, assim como muitos, testava meu código inserindo 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. O pytest se diferencia de outras ferramentas porque é minimalista: você escreve funções Python normais com nomes começados em , 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. Fundamentos: Escrevendo Seu Primeiro Teste Instalação e Primeiro Teste A instalação é simples — use pip para adicionar pytest ao seu projeto: Agora, crie um arquivo chamado : Execute com ou

<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&quot;Esperado 90.0, mas obtive {resultado}&quot;</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 {

&quot;id&quot;: 1,

&quot;nome&quot;: &quot;João Silva&quot;,

&quot;email&quot;: &quot;joao@example.com&quot;,

&quot;ativo&quot;: True

}

@pytest.fixture

def lista_usuarios():

return [

{&quot;id&quot;: 1, &quot;nome&quot;: &quot;João&quot;, &quot;email&quot;: &quot;joao@example.com&quot;},

{&quot;id&quot;: 2, &quot;nome&quot;: &quot;Maria&quot;, &quot;email&quot;: &quot;maria@example.com&quot;},

{&quot;id&quot;: 3, &quot;nome&quot;: &quot;Pedro&quot;, &quot;email&quot;: &quot;pedro@example.com&quot;},

]</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[&quot;email&quot;] == &quot;joao@example.com&quot;

def test_usuario_ativo(usuario_padrao):

assert usuario_padrao[&quot;ativo&quot;] 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(&quot;:memory:&quot;)

cursor = conexao.cursor()

cursor.execute(&quot;&quot;&quot;

CREATE TABLE usuarios (

id INTEGER PRIMARY KEY,

nome TEXT NOT NULL,

email TEXT UNIQUE

)

&quot;&quot;&quot;)

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(&quot;INSERT INTO usuarios (nome, email) VALUES (?, ?)&quot;,

(&quot;João&quot;, &quot;joao@example.com&quot;))

db_conexao.commit()

cursor.execute(&quot;SELECT * FROM usuarios WHERE email = ?&quot;,

(&quot;joao@example.com&quot;,))

resultado = cursor.fetchone()

assert resultado is not None

assert resultado[1] == &quot;João&quot;</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=&quot;function&quot;) # Padrão: nova instância por teste

def recurso_por_funcao():

return {&quot;valor&quot;: &quot;novo&quot;}

@pytest.fixture(scope=&quot;module&quot;) # Uma instância para todos os testes do módulo

def recurso_modulo():

return sqlite3.connect(&quot;:memory:&quot;)

@pytest.fixture(scope=&quot;session&quot;) # Uma instância para toda a sessão de testes

def recurso_sessao():

return {&quot;config&quot;: &quot;global&quot;}

def test_um(recurso_por_funcao):

recurso_por_funcao[&quot;valor&quot;] = &quot;modificado&quot;

def test_dois(recurso_por_funcao):

Aqui recurso_por_funcao é uma nova instância, não afetada pelo test_um

assert recurso_por_funcao[&quot;valor&quot;] == &quot;novo&quot;</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=[

{&quot;entrada&quot;: 2, &quot;esperado&quot;: 4},

{&quot;entrada&quot;: 3, &quot;esperado&quot;: 9},

{&quot;entrada&quot;: -1, &quot;esperado&quot;: 1},

])

def casos_potencia(request):

return request.param

def quadrado(n):

return n ** 2

def test_potencia(casos_potencia):

assert quadrado(casos_potencia[&quot;entrada&quot;]) == casos_potencia[&quot;esperado&quot;]</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 &quot;@&quot; in self.email

class TestUsuario:

@pytest.fixture

def usuario(self):

return Usuario(&quot;João&quot;, &quot;joao@example.com&quot;)

def test_criacao(self, usuario):

assert usuario.nome == &quot;João&quot;

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(&quot;Maria&quot;, &quot;maria_sem_email&quot;)

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 &quot;not lento&quot;</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(&quot;Divisão por zero não permitida&quot;)

return a / b

def test_divisao_por_zero():

with pytest.raises(ValueError, match=&quot;Divisão por zero&quot;):

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 = {&quot;usuario_id&quot;: usuario_id, &quot;itens&quot;: 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, [&quot;item1&quot;, &quot;item2&quot;])

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[&quot;usuario_id&quot;] == 1

assert len(args[&quot;itens&quot;]) == 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(&quot;entrada,esperado&quot;, [

(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: &quot;o que estou testando e por quê?&quot; 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

O que Todo Dev Deve Saber sobre Web Scraping em Python: requests, BeautifulSoup e Selenium
O que Todo Dev Deve Saber sobre Web Scraping em Python: requests, BeautifulSoup e Selenium

O que é Web Scraping e Por Que Aprender Web scraping é a técnica de extrair d...

Como Usar Strings em Python: Métodos, f-strings, raw strings e Formatação em Produção
Como Usar Strings em Python: Métodos, f-strings, raw strings e Formatação em Produção

Introdução: Por Que Dominar Strings em Python? Strings são um dos tipos de da...

Dominando Mypy em Python: Verificação Estática de Tipos no Projeto Real em Projetos Reais
Dominando Mypy em Python: Verificação Estática de Tipos no Projeto Real em Projetos Reais

Introdução ao Mypy: Por Que Type Checking Importa Quando você trabalha em pro...