DEV Community

Cover image for Framework de Testes Automatizados com Pytest API: Tutorial Prático
Lucas
Lucas

Posted on • Originally published at apidog.com

Framework de Testes Automatizados com Pytest API: Tutorial Prático

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.

Experimente o Apidog hoje

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 Test e 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Execute:

pytest -v
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Você também pode validar tempo de resposta com um limite simples:

assert response.elapsed.total_seconds() < 1.0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"},
    },
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Execute apenas smoke tests:

pytest -m smoke
Enter fullscreen mode Exit fullscreen mode

Execute tudo exceto testes lentos:

pytest -m "not slow"
Enter fullscreen mode Exit fullscreen mode

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)