Desenvolvedores Python usam o pytest porque ele reduz boilerplate: um teste é uma função test_*, uma asserção é um assert, e o executor cuida da descoberta e do relatório de falhas. Com requests, você consegue montar um conjunto de testes de API direto no código, sem depender de frameworks pesados.
Neste guia, você vai criar um conjunto de testes de API com pytest: configurar o projeto, escrever requisições HTTP, reutilizar setup com fixtures, testar múltiplas entradas com parametrize, validar status code, corpo da resposta e Schema JSON. Os exemplos usam uma API fictícia realista para que você possa adaptar a estrutura ao seu projeto.
Configurando o projeto
Crie um ambiente virtual e instale as dependências:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Use uma estrutura simples para manter os testes organizados:
api-tests/
conftest.py # fixtures compartilhadas
test_users.py # testes dos endpoints de usuários
test_orders.py # testes dos endpoints de pedidos
pytest.ini # configuração do pytest
O pytest descobre testes automaticamente quando você segue estas convenções:
- arquivos começam com
test_ou terminam com_test.py; - funções começam com
test_; - classes começam com
Teste não possuem__init__.
Exemplo de configuração inicial em pytest.ini:
[pytest]
addopts = -v
testpaths = .
markers =
smoke: testes rápidos de verificação principal
slow: testes mais demorados
Se automação de testes ainda é um tema novo para você, este guia sobre o que é teste automatizado ajuda a contextualizar.
Por que usar pytest para testes de API? Porque requests resolve a camada HTTP e o pytest resolve o restante: descoberta, asserções legíveis, setup e teardown com fixtures, testes orientados por dados com parametrize e relatórios. Assim, os testes podem ficar no mesmo repositório da aplicação e falhar na mesma pull request que introduziu a mudança.
Escrevendo seu primeiro teste de API
Um teste básico envia uma requisição e valida a resposta:
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
Agora valide campos do corpo JSON:
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
Execute:
pytest -v
Quando um assert falha, o pytest mostra o valor real e o esperado sem exigir métodos especiais de asserção. Para ampliar a cobertura das verificações, veja este guia sobre asserções de API.
Compartilhando configuração com fixtures
Evite repetir BASE_URL, headers e sessões HTTP em todos os testes. Use fixtures.
Crie um conftest.py:
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
Agora qualquer teste pode receber api_session como argumento:
def test_get_user(api_session):
response = api_session.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
Para autenticação, crie outra fixture:
# conftest.py
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={
"email": "qa@example.com",
"password": "test-pass",
},
)
assert response.status_code == 200
return response.json()["token"]
Use o token no teste:
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
O scope="session" cria a sessão uma vez por execução. O yield separa setup e teardown: o código antes de yield prepara o recurso; o código depois limpa o recurso quando ele sai do escopo.
Fixtures substituem bem padrões antigos como setup_function e teardown_function, porque são composáveis, explícitas e suportam escopos. A documentação oficial de fixtures do pytest recomenda esse modelo como abordagem padrão.
Executando o mesmo teste com múltiplas entradas
APIs precisam ser testadas com entradas válidas, inválidas e casos de borda. Em vez de criar várias funções quase iguais, use @pytest.mark.parametrize:
import pytest
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize(
"user_id, expected_status",
[
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
],
)
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
O pytest gera quatro casos independentes. Se um falhar, os outros continuam sendo executados e reportados.
Quando a lista de entradas crescer, carregue os dados de um arquivo CSV ou JSON. Este guia sobre testes de API orientados por dados com CSV e JSON cobre esse padrão. Se você precisa revisar qual status code usar em cada cenário, consulte a referência sobre códigos de status HTTP que APIs REST devem usar.
Fazendo asserções no corpo da resposta
Status code é necessário, mas não suficiente. Uma resposta 200 com JSON incorreto ainda é um bug.
Exemplo:
def test_order_response_body(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert response.status_code == 201
assert isinstance(body["id"], int)
assert body["product_id"] == 7
assert body["quantity"] == 2
assert body["total"] > 0
assert body["status"] == "pending"
Você também pode validar tempo de resposta com um limite simples:
assert response.elapsed.total_seconds() < 1.0
Mantenha esse limite generoso para evitar falhas intermitentes causadas por variação normal de rede.
Validando Schema JSON
Para capturar campos ausentes, tipos incorretos ou mudanças estruturais, valide a resposta contra um Schema JSON.
Instale a dependência, se ainda não instalou:
pip install jsonschema
Defina o schema:
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
Use no teste:
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
validate(instance=response.json(), schema=order_schema)
A validação por schema escala melhor do que validar campo por campo em todos os testes. A biblioteca jsonschema é uma opção comum em Python, e a documentação de validação lista as palavras-chave suportadas.
Usando variáveis de ambiente
Não codifique URLs, usuários, senhas ou tokens nos testes. Leia valores sensíveis de variáveis de ambiente.
Exemplo:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Para credenciais:
import os
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={
"email": os.environ["API_USER_EMAIL"],
"password": os.environ["API_USER_PASSWORD"],
},
)
assert response.status_code == 200
return response.json()["token"]
Isso permite executar o mesmo conjunto de testes localmente, em staging ou na CI sem editar código.
Executando os testes na CI
O pytest retorna código de saída diferente de zero quando há falha. Isso já é suficiente para quebrar um pipeline.
Com relatório JUnit:
pytest -v --junitxml=results.xml
Exemplo básico com GitHub Actions:
name: API tests
on:
pull_request:
push:
branches:
- main
jobs:
pytest-api:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest requests jsonschema
- name: Run API tests
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
API_USER_EMAIL: ${{ secrets.API_USER_EMAIL }}
API_USER_PASSWORD: ${{ secrets.API_USER_PASSWORD }}
run: |
pytest -v --junitxml=results.xml
Para uma configuração mais completa, incluindo secrets e seleção de ambiente, veja o guia sobre testes de API em pipelines de CI/CD.
Executando testes em paralelo
Quando o conjunto crescer, use pytest-xdist para reduzir o tempo de feedback:
pip install pytest-xdist
pytest -n auto
Isso distribui testes entre os núcleos da CPU.
Para funcionar bem, seus testes precisam ser independentes:
- não dependa da ordem de execução;
- crie dados próprios para cada teste;
- limpe recursos criados durante o teste;
- evite compartilhar estado mutável entre testes.
Um conjunto que depende de ordem tende a falhar de forma imprevisível quando executado em paralelo.
Mantendo o conjunto de testes sustentável
Um conjunto com 50 testes é fácil de manter. Um com 500 exige disciplina.
Organize por domínio
Prefira arquivos focados:
test_users.py
test_orders.py
test_payments.py
Evite um arquivo gigante com todos os endpoints.
Use markers
Registre markers no pytest.ini:
[pytest]
markers =
smoke: testes críticos e rápidos
slow: testes demorados
Use nos testes:
import pytest
@pytest.mark.smoke
def test_get_user(api_session):
response = api_session.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
Execute apenas smoke tests:
pytest -m smoke
Execute tudo exceto testes lentos:
pytest -m "not slow"
Centralize configuração e helpers
Mantenha em um único lugar:
-
BASE_URL; - fixtures;
- schemas;
- funções auxiliares;
- criação e limpeza de dados de teste.
Se você copiar o mesmo código duas vezes, extraia para fixture ou função auxiliar. A mesma disciplina modular descrita neste guia sobre como escrever scripts de teste automatizados se aplica a pytest.
Quando optar por uma plataforma
Um framework pytest funciona bem quando sua equipe escreve Python e quer testes próximos ao código da aplicação. Ele é menos conveniente quando QA, produto ou outras áreas precisam contribuir sem escrever fixtures, helpers e asserções manualmente.
Apidog cobre esse espaço com construção visual de testes, validação de schema contra especificação OpenAPI, execuções orientadas por dados com CSV e JSON, e executor CLI para CI. Muitas equipes usam os dois: pytest para cenários com lógica complexa e Apidog para cobertura ampla, design e simulação das APIs que os testes pytest validam. Você pode baixar o Apidog e comparar as abordagens em um endpoint real.
Perguntas frequentes
Por que usar pytest em vez do unittest embutido do Python para testes de API?
O pytest exige menos boilerplate. Testes são funções simples, asserções usam assert, fixtures lidam com setup de forma flexível e parametrize facilita testes orientados por dados. Ele também consegue executar testes existentes no estilo unittest, o que reduz o risco de migração.
Qual é a diferença entre fixture e parametrize?
Uma fixture fornece um recurso reutilizável, como sessão HTTP ou token de autenticação. parametrize executa o mesmo teste várias vezes com entradas diferentes. Eles se complementam: um teste parametrizado pode receber fixtures normalmente.
Devo fazer asserções sobre tempo de resposta?
Você pode usar response.elapsed.total_seconds() para detectar regressões grandes. Mas pytest é uma ferramenta de teste funcional, não de carga. Para testes reais de performance, use uma ferramenta dedicada e mantenha limites de tempo flexíveis nos testes funcionais.
Como manter testes de API independentes?
Crie dados específicos para cada teste, limpe recursos após a execução e não dependa da ordem dos testes. Testes independentes podem rodar em paralelo e falham de forma isolada, o que facilita a depuração.
O pytest pode validar respostas contra uma especificação OpenAPI?
O pytest sozinho não faz essa validação. Você pode validar JSON com jsonschema ou usar plugins que comparam respostas com um documento OpenAPI. Se validação de schema contra OpenAPI for parte central do fluxo, uma plataforma como Apidog pode reduzir a configuração manual.
Top comments (0)