Programador Leigo
API 10 min leitura 15 MAR 2026

Como testar e documentar APIs Python

Teste suas APIs com pytest e documente com Swagger/OpenAPI para que outros devs consigam usar.


Por que testar APIs?

Testar APIs manualmente com curl ou Postman funciona durante o desenvolvimento, mas não escala. Conforme sua API cresce, você precisa garantir que alterações em uma rota não quebrem outras. Testes automatizados resolvem isso: rodam em segundos, cobrem dezenas de cenários e podem ser integrados ao seu pipeline de deploy.

Uma API sem testes é uma API que vai quebrar em produção. É só questão de tempo.


Configurando o ambiente de testes

pip install pytest

Vamos usar como base a API de tarefas que criamos nos artigos anteriores. Primeiro, precisamos reestruturar o código para que a aplicação Flask seja facilmente importável nos testes.

app.py (refatorado)

from flask import Flask, jsonify, request


def create_app():
    app = Flask(__name__)

    tarefas = [
        {"id": 1, "titulo": "Estudar Flask", "descricao": "APIs REST", "concluida": False},
        {"id": 2, "titulo": "Praticar Python", "descricao": "Exercicios", "concluida": True},
    ]
    app.config["tarefas"] = tarefas
    app.config["proximo_id"] = 3

    @app.route("/api/tarefas", methods=["GET"])
    def listar_tarefas():
        return jsonify(app.config["tarefas"]), 200

    @app.route("/api/tarefas/<int:tarefa_id>", methods=["GET"])
    def buscar_tarefa(tarefa_id):
        tarefa = next(
            (t for t in app.config["tarefas"] if t["id"] == tarefa_id),
            None
        )
        if tarefa is None:
            return jsonify({"erro": "Tarefa nao encontrada"}), 404
        return jsonify(tarefa), 200

    @app.route("/api/tarefas", methods=["POST"])
    def criar_tarefa():
        dados = request.get_json()
        if not dados or "titulo" not in dados:
            return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400

        nova_tarefa = {
            "id": app.config["proximo_id"],
            "titulo": dados["titulo"],
            "descricao": dados.get("descricao", ""),
            "concluida": False
        }
        app.config["tarefas"].append(nova_tarefa)
        app.config["proximo_id"] += 1
        return jsonify(nova_tarefa), 201

    @app.route("/api/tarefas/<int:tarefa_id>", methods=["PUT"])
    def atualizar_tarefa(tarefa_id):
        tarefa = next(
            (t for t in app.config["tarefas"] if t["id"] == tarefa_id),
            None
        )
        if tarefa is None:
            return jsonify({"erro": "Tarefa nao encontrada"}), 404

        dados = request.get_json()
        if not dados or "titulo" not in dados:
            return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400

        tarefa["titulo"] = dados["titulo"]
        tarefa["descricao"] = dados.get("descricao", "")
        tarefa["concluida"] = dados.get("concluida", False)
        return jsonify(tarefa), 200

    @app.route("/api/tarefas/<int:tarefa_id>", methods=["DELETE"])
    def deletar_tarefa(tarefa_id):
        tarefa = next(
            (t for t in app.config["tarefas"] if t["id"] == tarefa_id),
            None
        )
        if tarefa is None:
            return jsonify({"erro": "Tarefa nao encontrada"}), 404

        app.config["tarefas"] = [
            t for t in app.config["tarefas"] if t["id"] != tarefa_id
        ]
        return "", 204

    return app


if __name__ == "__main__":
    app = create_app()
    app.run(debug=True)

Escrevendo os primeiros testes

O Flask fornece um test client embutido que simula requisições HTTP sem precisar iniciar o servidor. Crie um arquivo test_app.py:

Fixture do pytest

import pytest
from app import create_app


@pytest.fixture
def client():
    app = create_app()
    app.config["TESTING"] = True

    with app.test_client() as client:
        yield client

A fixture client cria uma nova instância da aplicação para cada teste. Isso garante isolamento — os dados manipulados em um teste não afetam outro.


Testando o endpoint GET (listar)

def test_listar_tarefas(client):
    resposta = client.get("/api/tarefas")

    assert resposta.status_code == 200
    dados = resposta.get_json()
    assert isinstance(dados, list)
    assert len(dados) == 2


def test_listar_retorna_json(client):
    resposta = client.get("/api/tarefas")

    assert resposta.content_type == "application/json"

Testando o endpoint GET (buscar por ID)

def test_buscar_tarefa_existente(client):
    resposta = client.get("/api/tarefas/1")

    assert resposta.status_code == 200
    dados = resposta.get_json()
    assert dados["id"] == 1
    assert dados["titulo"] == "Estudar Flask"


def test_buscar_tarefa_inexistente(client):
    resposta = client.get("/api/tarefas/999")

    assert resposta.status_code == 404
    dados = resposta.get_json()
    assert "erro" in dados

Testando o endpoint POST (criar)

def test_criar_tarefa_valida(client):
    nova_tarefa = {
        "titulo": "Nova tarefa de teste",
        "descricao": "Criada pelo pytest"
    }

    resposta = client.post(
        "/api/tarefas",
        json=nova_tarefa
    )

    assert resposta.status_code == 201
    dados = resposta.get_json()
    assert dados["titulo"] == "Nova tarefa de teste"
    assert dados["descricao"] == "Criada pelo pytest"
    assert dados["concluida"] is False
    assert "id" in dados


def test_criar_tarefa_sem_titulo(client):
    resposta = client.post(
        "/api/tarefas",
        json={"descricao": "Sem titulo"}
    )

    assert resposta.status_code == 400
    dados = resposta.get_json()
    assert "erro" in dados


def test_criar_tarefa_sem_json(client):
    resposta = client.post("/api/tarefas")

    assert resposta.status_code == 400


def test_criar_tarefa_aparece_na_lista(client):
    client.post("/api/tarefas", json={"titulo": "Tarefa extra"})

    resposta = client.get("/api/tarefas")
    dados = resposta.get_json()
    assert len(dados) == 3

Testando o endpoint PUT (atualizar)

def test_atualizar_tarefa(client):
    dados_atualizados = {
        "titulo": "Tarefa atualizada",
        "descricao": "Descricao nova",
        "concluida": True
    }

    resposta = client.put("/api/tarefas/1", json=dados_atualizados)

    assert resposta.status_code == 200
    dados = resposta.get_json()
    assert dados["titulo"] == "Tarefa atualizada"
    assert dados["concluida"] is True


def test_atualizar_tarefa_inexistente(client):
    resposta = client.put(
        "/api/tarefas/999",
        json={"titulo": "Nao existe"}
    )

    assert resposta.status_code == 404


def test_atualizar_tarefa_sem_titulo(client):
    resposta = client.put(
        "/api/tarefas/1",
        json={"descricao": "Sem titulo"}
    )

    assert resposta.status_code == 400

Testando o endpoint DELETE

def test_deletar_tarefa(client):
    resposta = client.delete("/api/tarefas/1")

    assert resposta.status_code == 204

    # verificar que foi realmente removida
    resposta = client.get("/api/tarefas/1")
    assert resposta.status_code == 404


def test_deletar_tarefa_inexistente(client):
    resposta = client.delete("/api/tarefas/999")

    assert resposta.status_code == 404

Testando autenticação

Se sua API usa JWT (como vimos no artigo anterior), também é preciso testar as rotas protegidas:

import jwt
from datetime import datetime, timedelta, timezone


def gerar_token_teste(app, user_id=1):
    return jwt.encode(
        {
            "user_id": user_id,
            "exp": datetime.now(timezone.utc) + timedelta(hours=1)
        },
        app.config.get("SECRET_KEY", "chave-teste"),
        algorithm="HS256"
    )


def test_rota_protegida_sem_token(client):
    resposta = client.get("/api/perfil")

    assert resposta.status_code == 401


def test_rota_protegida_com_token_valido(client):
    token = gerar_token_teste(client.application)

    resposta = client.get(
        "/api/perfil",
        headers={"Authorization": f"Bearer {token}"}
    )

    assert resposta.status_code == 200


def test_rota_protegida_com_token_expirado(client):
    token = jwt.encode(
        {
            "user_id": 1,
            "exp": datetime.now(timezone.utc) - timedelta(hours=1)
        },
        client.application.config.get("SECRET_KEY", "chave-teste"),
        algorithm="HS256"
    )

    resposta = client.get(
        "/api/perfil",
        headers={"Authorization": f"Bearer {token}"}
    )

    assert resposta.status_code == 401

Rodando os testes

# rodar todos os testes
pytest test_app.py -v

# rodar com saida detalhada
pytest test_app.py -v --tb=short

# rodar apenas um teste especifico
pytest test_app.py::test_criar_tarefa_valida -v

# ver cobertura de codigo
pip install pytest-cov
pytest test_app.py --cov=app --cov-report=term-missing

Saída esperada:

test_app.py::test_listar_tarefas PASSED
test_app.py::test_listar_retorna_json PASSED
test_app.py::test_buscar_tarefa_existente PASSED
test_app.py::test_buscar_tarefa_inexistente PASSED
test_app.py::test_criar_tarefa_valida PASSED
test_app.py::test_criar_tarefa_sem_titulo PASSED
test_app.py::test_criar_tarefa_sem_json PASSED
test_app.py::test_criar_tarefa_aparece_na_lista PASSED
test_app.py::test_atualizar_tarefa PASSED
test_app.py::test_atualizar_tarefa_inexistente PASSED
test_app.py::test_deletar_tarefa PASSED
test_app.py::test_deletar_tarefa_inexistente PASSED

============ 12 passed in 0.15s ============

Documentação com Swagger/OpenAPI

Testes garantem que sua API funciona. Documentação garante que outros desenvolvedores consigam usá-la. O padrão OpenAPI (antes chamado Swagger) é o formato mais usado para documentar APIs REST.

O que é OpenAPI?

OpenAPI é uma especificação em YAML ou JSON que descreve todos os endpoints, parâmetros, respostas e modelos de dados da sua API. Com essa especificação, ferramentas geram interfaces visuais interativas automaticamente.


Documentação automática com FastAPI

O FastAPI gera documentação Swagger e ReDoc automaticamente, sem nenhuma configuração extra. Se você está começando um projeto novo e documentação é prioridade, considere o FastAPI.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI(
    title="API de Tarefas",
    description="API para gerenciar tarefas do dia a dia",
    version="1.0.0"
)


class TarefaEntrada(BaseModel):
    titulo: str
    descricao: Optional[str] = ""
    concluida: Optional[bool] = False


class TarefaSaida(BaseModel):
    id: int
    titulo: str
    descricao: str
    concluida: bool


tarefas = [
    {"id": 1, "titulo": "Estudar FastAPI", "descricao": "Documentacao", "concluida": False}
]
proximo_id = 2


@app.get("/api/tarefas", response_model=list[TarefaSaida])
def listar_tarefas():
    """Retorna a lista completa de tarefas cadastradas."""
    return tarefas


@app.get("/api/tarefas/{tarefa_id}", response_model=TarefaSaida)
def buscar_tarefa(tarefa_id: int):
    """Busca uma tarefa pelo ID. Retorna 404 se nao encontrada."""
    tarefa = next((t for t in tarefas if t["id"] == tarefa_id), None)
    if tarefa is None:
        raise HTTPException(status_code=404, detail="Tarefa nao encontrada")
    return tarefa


@app.post("/api/tarefas", response_model=TarefaSaida, status_code=201)
def criar_tarefa(tarefa: TarefaEntrada):
    """Cria uma nova tarefa. O campo titulo e obrigatorio."""
    global proximo_id
    nova = {
        "id": proximo_id,
        "titulo": tarefa.titulo,
        "descricao": tarefa.descricao,
        "concluida": tarefa.concluida
    }
    tarefas.append(nova)
    proximo_id += 1
    return nova

Rode com uvicorn app:app --reload e acesse:

  • Swagger UI: http://127.0.0.1:8000/docs
  • ReDoc: http://127.0.0.1:8000/redoc

A documentação é gerada a partir dos type hints, dos modelos Pydantic e das docstrings das funções. Qualquer alteração no código atualiza a documentação automaticamente.


Documentando APIs Flask com flasgger

Se você está usando Flask e quer documentação Swagger, o flasgger é a opção mais direta.

pip install flasgger
from flask import Flask, jsonify, request
from flasgger import Swagger

app = Flask(__name__)
swagger = Swagger(app, template={
    "info": {
        "title": "API de Tarefas",
        "description": "API REST para gerenciar tarefas",
        "version": "1.0.0"
    }
})

tarefas = [
    {"id": 1, "titulo": "Estudar Flask", "descricao": "APIs REST", "concluida": False}
]


@app.route("/api/tarefas", methods=["GET"])
def listar_tarefas():
    """Listar todas as tarefas
    ---
    responses:
      200:
        description: Lista de tarefas
        schema:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              titulo:
                type: string
              descricao:
                type: string
              concluida:
                type: boolean
    """
    return jsonify(tarefas), 200


@app.route("/api/tarefas", methods=["POST"])
def criar_tarefa():
    """Criar uma nova tarefa
    ---
    parameters:
      - in: body
        name: tarefa
        required: true
        schema:
          type: object
          required:
            - titulo
          properties:
            titulo:
              type: string
              example: Minha nova tarefa
            descricao:
              type: string
              example: Descricao da tarefa
    responses:
      201:
        description: Tarefa criada com sucesso
      400:
        description: Dados invalidos
    """
    dados = request.get_json()
    if not dados or "titulo" not in dados:
        return jsonify({"erro": "Campo 'titulo' e obrigatorio"}), 400

    nova_tarefa = {
        "id": len(tarefas) + 1,
        "titulo": dados["titulo"],
        "descricao": dados.get("descricao", ""),
        "concluida": False
    }
    tarefas.append(nova_tarefa)
    return jsonify(nova_tarefa), 201


if __name__ == "__main__":
    app.run(debug=True)

Acesse http://127.0.0.1:5000/apidocs para ver a interface Swagger gerada automaticamente.


Documentando com flask-smorest

O flask-smorest oferece uma abordagem mais moderna, usando marshmallow para validação e documentação simultaneamente:

pip install flask-smorest marshmallow
from flask import Flask
from flask.views import MethodView
from flask_smorest import Api, Blueprint, abort
from marshmallow import Schema, fields

app = Flask(__name__)
app.config["API_TITLE"] = "API de Tarefas"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/docs"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"

api = Api(app)


class TarefaSchema(Schema):
    id = fields.Int(dump_only=True)
    titulo = fields.Str(required=True)
    descricao = fields.Str(load_default="")
    concluida = fields.Bool(load_default=False)


blp = Blueprint("tarefas", __name__, url_prefix="/api/tarefas")
tarefas = []
proximo_id = 1


@blp.route("/")
class TarefaLista(MethodView):
    @blp.response(200, TarefaSchema(many=True))
    def get(self):
        """Listar todas as tarefas"""
        return tarefas

    @blp.arguments(TarefaSchema)
    @blp.response(201, TarefaSchema)
    def post(self, dados):
        """Criar nova tarefa"""
        global proximo_id
        tarefa = {
            "id": proximo_id,
            "titulo": dados["titulo"],
            "descricao": dados.get("descricao", ""),
            "concluida": dados.get("concluida", False)
        }
        tarefas.append(tarefa)
        proximo_id += 1
        return tarefa


api.register_blueprint(blp)

O flask-smorest gera a especificação OpenAPI 3.0 a partir dos schemas marshmallow, eliminando a duplicação entre validação e documentação.


Organizando testes em um projeto real

Para projetos maiores, organize os testes em uma pasta dedicada:

api-tarefas/
├── app/
│   ├── __init__.py
│   └── tarefas/
│       ├── __init__.py
│       └── routes.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_tarefas.py
│   └── test_auth.py
├── run.py
└── requirements.txt

tests/conftest.py

import pytest
from app import create_app


@pytest.fixture
def app():
    app = create_app()
    app.config["TESTING"] = True
    return app


@pytest.fixture
def client(app):
    return app.test_client()

As fixtures definidas em conftest.py ficam disponíveis automaticamente para todos os arquivos de teste na mesma pasta.


Comparação: ferramentas de documentação

Ferramenta Framework OpenAPI Validação integrada Dificuldade
FastAPI (nativo) FastAPI 3.0 Sim (Pydantic) Baixa
flasgger Flask 2.0/3.0 Não Baixa
flask-smorest Flask 3.0 Sim (marshmallow) Média
Manualmente Qualquer Qualquer Não Alta

Resumo

Conceito O que aprendemos
pytest Framework de testes para Python
test_client Cliente de testes embutido do Flask
Fixtures Funções que preparam o ambiente de teste
OpenAPI/Swagger Especificação padrão para documentar APIs
flasgger Documentação Swagger para Flask via docstrings
flask-smorest Documentação OpenAPI 3.0 com marshmallow
FastAPI docs Documentação automática via type hints

Testar e documentar não são tarefas extras — são parte essencial do desenvolvimento de APIs profissionais. Uma API bem testada gera confiança para fazer alterações. Uma API bem documentada permite que outros desenvolvedores integrem rapidamente sem precisar ler seu código.

Continue lendo

Compartilhar