Como testar uma API Flask com pytest e test_client

Teste uma API Flask sem iniciar servidor: use pytest, fixtures e test_client para validar JSON, erros HTTP e isolamento com exemplos executáveis.

01 Oct 2026 8 min de leitura Equipe Python Dev BR

Para testar uma API Flask com pytest, crie uma aplicação com TESTING=True, obtenha um app.test_client() e faça requisições como client.get() e client.post(json=...). Você verifica status HTTP e conteúdo JSON sem iniciar o servidor nem ocupar uma porta. Fixtures permitem criar uma aplicação nova para cada teste, evitando que dados de um caso contaminem o próximo.

Neste tutorial vamos testar uma pequena API de vagas Python: cadastro, listagem e validação de entrada. É um exercício útil para quem já fez o primeiro projeto Flask e quer adicionar testes demonstráveis ao portfólio de backend.

O que o test_client testa — e o que fica de fora

O cliente de testes passa requisições pela aplicação Flask, incluindo roteamento, funções de view e hooks registrados. Ele trabalha no próprio processo, sem fazer uma chamada HTTP pela rede.

Ferramenta ou abordagemO que verificaQuando usar
pytest com funções isoladasRegras e transformações sem HTTPTestes unitários de lógica de negócio
Flask test_clientRotas, validações, respostas e integração dentro da aplicaçãoDesenvolvimento diário da API
requests ou HTTPX contra servidor realHTTP pela rede e serviço em execuçãoSmoke tests de ambiente isolado
PlaywrightFluxo no navegador, interface e interaçãoTestes de ponta a ponta do frontend

test_client não valida TLS, configuração do proxy reverso nem se o processo subiu corretamente no deploy. Também não substitui testes do banco: se você simular a persistência, não estará verificando SQL nem constraints.

A proposta aqui é pequena de propósito: Flask, pytest e armazenamento em memória. Não há autenticação, banco ou serviço externo. Não publique esta API como um mural real de vagas sem adicionar esses controles.

1. Prepare o projeto

Use Python 3.10 ou superior e um ambiente virtual. No terminal:

mkdir flask-vagas-testes
cd flask-vagas-testes
python -m venv .venv

Ative o ambiente conforme o sistema:

# Linux ou macOS
source .venv/bin/activate

# Windows PowerShell
.venv\Scripts\Activate.ps1

Instale as dependências:

python -m pip install Flask pytest

A estrutura final será:

flask-vagas-testes/
├── app.py
└── tests/
    ├── conftest.py
    └── test_vagas.py

Execute os comandos sempre da pasta flask-vagas-testes. Não dê ao seu arquivo o nome flask.py ou pytest.py: isso pode esconder o pacote instalado durante o import.

2. Crie a API com uma application factory

Salve este código completo em app.py:

from flask import Flask, jsonify, request


def create_app(config=None):
    app = Flask(__name__)
    if config is not None:
        app.config.update(config)

    # Cada chamada de create_app ganha sua própria lista.
    vagas = []

    @app.get("/vagas")
    def listar_vagas():
        return jsonify({"vagas": vagas})

    @app.post("/vagas")
    def criar_vaga():
        if not request.is_json:
            return jsonify({"erro": "Envie Content-Type application/json"}), 415

        dados = request.get_json(silent=True)
        if not isinstance(dados, dict):
            return jsonify({"erro": "Envie um objeto JSON válido"}), 400

        titulo = dados.get("titulo")
        if not isinstance(titulo, str) or not titulo.strip():
            return jsonify({"erro": "titulo deve ser um texto não vazio"}), 400

        vaga = {"id": len(vagas) + 1, "titulo": titulo.strip()}
        vagas.append(vaga)
        return jsonify(vaga), 201

    return app

Uma application factory é uma função que cria e configura uma instância da aplicação. Em vez de importar um único objeto global, o teste chama create_app() e recebe um estado novo.

A lista vagas está dentro da factory, não no topo do módulo. Isso faz diferença: uma lista global sobreviveria entre aplicações e poderia tornar os testes dependentes da ordem de execução. Esse repositório é apenas didático; os dados desaparecem ao recriar a aplicação e não são compartilhados entre workers.

A API distingue três situações:

  • 201: uma vaga foi criada;
  • 400: o corpo JSON ou o título é inválido;
  • 415: o cliente não enviou um tipo de conteúdo JSON.

get_json(silent=True) retorna None quando não consegue interpretar o corpo. Como queremos um contrato explícito de erro, verificamos o resultado e devolvemos nossa resposta JSON. Uma lista JSON válida também é rejeitada: o contrato exige um objeto.

3. Configure fixtures para aplicação e cliente

Crie a pasta tests e salve este código em tests/conftest.py:

import pytest

from app import create_app


@pytest.fixture()
def app():
    return create_app({"TESTING": True})


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

O pytest descobre fixtures em conftest.py automaticamente. Um teste que declara o parâmetro client recebe o resultado dessa fixture; não precisa importar conftest.

O escopo padrão é function: a cada teste, uma nova aplicação e um novo cliente são criados. Evite mudar para scope="session" só para economizar linhas ou alguns milissegundos; estado compartilhado pode introduzir falhas intermitentes.

TESTING=True permite, entre outros comportamentos, que exceções não tratadas cheguem ao teste em vez de serem escondidas atrás de uma resposta genérica de erro. Ele não desativa automaticamente autenticação, CSRF ou chamadas externas.

4. Teste listagem e criação com assertivas específicas

Salve em tests/test_vagas.py:

import pytest


def test_listagem_comeca_vazia(client):
    resposta = client.get("/vagas")

    assert resposta.status_code == 200
    assert resposta.is_json
    assert resposta.get_json() == {"vagas": []}


def test_cria_vaga_e_lista_resultado(client):
    resposta = client.post(
        "/vagas", json={"titulo": "  Backend Python — remoto Brasil  "}
    )

    assert resposta.status_code == 201
    vaga = resposta.get_json()
    assert vaga == {"id": 1, "titulo": "Backend Python — remoto Brasil"}

    listagem = client.get("/vagas")
    assert listagem.status_code == 200
    assert listagem.get_json() == {"vagas": [vaga]}


@pytest.mark.parametrize("titulo", [None, "", "   ", 123, ["Python"]])
def test_rejeita_titulo_invalido(client, titulo):
    resposta = client.post("/vagas", json={"titulo": titulo})

    assert resposta.status_code == 400
    assert resposta.get_json() == {
        "erro": "titulo deve ser um texto não vazio"
    }
    assert client.get("/vagas").get_json() == {"vagas": []}


def test_rejeita_titulo_ausente(client):
    resposta = client.post("/vagas", json={})

    assert resposta.status_code == 400
    assert resposta.get_json()["erro"] == "titulo deve ser um texto não vazio"

O argumento json= serializa o objeto e configura o cabeçalho de conteúdo. Evite misturar json= e data= na mesma requisição.

Observe que o teste de criação não confere apenas 201: ele verifica o título normalizado e consulta a listagem. Assim detecta uma implementação que retorna sucesso, mas esquece de guardar o registro.

Nos testes negativos também conferimos que a lista continua vazia. Rejeitar a entrada depois de já alterar o estado seria um bug importante que uma assertiva de status sozinha não encontraria.

A parametrização transforma cinco entradas inválidas em cinco casos separados. Veja mais padrões no guia de testes com pytest.

5. Cubra JSON malformado e método incorreto

Acrescente os testes abaixo ao mesmo arquivo tests/test_vagas.py:

@pytest.mark.parametrize("corpo", ["{", "[]", "null"])
def test_rejeita_corpo_json_invalido(client, corpo):
    resposta = client.post(
        "/vagas", data=corpo, content_type="application/json"
    )

    assert resposta.status_code == 400
    assert resposta.get_json() == {"erro": "Envie um objeto JSON válido"}


def test_rejeita_formulario(client):
    resposta = client.post("/vagas", data={"titulo": "Python júnior"})

    assert resposta.status_code == 415
    assert resposta.get_json() == {
        "erro": "Envie Content-Type application/json"
    }


def test_metodo_nao_permitido(client):
    resposta = client.delete("/vagas")

    assert resposta.status_code == 405


def test_rota_inexistente(client):
    resposta = client.get("/rota-inexistente")

    assert resposta.status_code == 404


def test_nova_aplicacao_nao_herda_vagas(app, client):
    from app import create_app

    resposta = client.post("/vagas", json={"titulo": "Pessoa desenvolvedora Python"})
    assert resposta.status_code == 201
    assert len(client.get("/vagas").get_json()["vagas"]) == 1

    outra_app = create_app({"TESTING": True})
    outro_client = outra_app.test_client()
    assert outro_client.get("/vagas").get_json() == {"vagas": []}

data= é útil quando queremos controlar o corpo cru e enviar conteúdo propositalmente malformado. { não é JSON válido; [] e null são JSON válido, mas não cumprem o contrato de objeto da nossa API.

Já os erros 404 e 405 são respostas padrão do Flask e podem vir em HTML. Por isso esses testes não chamam get_json() esperando nosso formato de erro. Se sua API promete JSON em todos os erros, implemente handlers e teste também esse contrato.

6. Execute e leia as falhas

Na raiz do projeto:

python -m pytest -q

Com os arquivos acima, a suíte deve mostrar 15 testes aprovados. A contagem inclui cada entrada parametrizada como um teste.

Para executar apenas os testes de título:

python -m pytest -q -k titulo

Para interromper na primeira falha e mostrar detalhes:

python -m pytest -x -v

Quando um teste falhar, compare o valor esperado com o recebido. Um 415 onde você esperava 201, por exemplo, pode indicar que enviou data= em vez de json=. Um 500 ou uma exceção propagada aponta para um problema na execução da rota, não necessariamente no pytest.

Para repetir exatamente o ambiente em outra máquina ou CI, registre as versões de dependências testadas no gerenciamento de pacotes do projeto. Não dependa apenas de instalar a versão mais recente a cada execução; o uv é uma opção para manter esse fluxo organizado.

Como adaptar para banco, login e serviços externos

Em uma aplicação real, preserve a factory e substitua a lista por uma camada de persistência. A fixture deve configurar um banco exclusivamente de testes antes de a aplicação inicializar suas extensões.

Cuidados essenciais:

  1. Banco: use dados sintéticos e limpe o estado entre testes. SQLite pode servir para alguns cenários, mas não reproduz todos os comportamentos do PostgreSQL. Valide operações importantes no mesmo motor usado em produção.
  2. Conexões: feche recursos no teardown, normalmente depois de yield na fixture. Em sqlite3, o contexto da conexão controla commit/rollback, mas não fecha a conexão automaticamente.
  3. Login: teste uma requisição anônima e outra autenticada. Para login por sessão, faça o fluxo de login pelo cliente e confira se o acesso protegido é realmente autorizado.
  4. APIs externas: injete um cliente falso ou use monkeypatch na fronteira de integração. Não faça a suíte diária depender da disponibilidade de um provedor nem enviar notificações reais.
  5. Exceções: simule falhas de dependências e verifique o comportamento prometido. Não transforme todo erro em 200 para facilitar o teste.

Se o objetivo for testar uma interface no navegador, continue com Playwright em Python. Para entender o próximo passo de persistência, veja SQLAlchemy.

Checklist antes de colocar os testes no portfólio

  • A suíte roda com um comando documentado na raiz do projeto.
  • Cada teste começa com estado conhecido e não depende do teste anterior.
  • Entradas válidas, ausentes, malformadas e com tipo errado estão cobertas.
  • Há assertivas de status e conteúdo relevante.
  • Nenhum teste usa credenciais, dados pessoais ou banco de produção.
  • Testes com mocks não são apresentados como prova de que uma integração real funciona.
  • O CI executa a suíte antes de aceitar a mudança.

Para uma API Flask pequena, esse conjunto já demonstra uma habilidade concreta: definir o contrato HTTP e verificar automaticamente se ele continua funcionando. Depois, amplie conforme os riscos da aplicação — autorização, persistência e integração — em vez de acumular testes que apenas repetem o caminho feliz.

Referências oficiais

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados