---
title: "Como testar uma API Flask com pytest e test_client"
url: "https://python.dev.br/blog/flask-pytest-test-client-testar-api/"
markdown_url: "https://python.dev.br/blog/flask-pytest-test-client-testar-api.MD"
description: "Teste uma API Flask sem iniciar servidor: use pytest, fixtures e test_client para validar JSON, erros HTTP e isolamento com exemplos executáveis."
date: "2026-10-01"
author: "Equipe Python Dev BR"
---

# 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.


**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](/guias/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 abordagem | O que verifica | Quando usar |
|---|---|---|
| pytest com funções isoladas | Regras e transformações sem HTTP | Testes unitários de lógica de negócio |
| Flask `test_client` | Rotas, validações, respostas e integração dentro da aplicação | Desenvolvimento diário da API |
| `requests` ou HTTPX contra servidor real | HTTP pela rede e serviço em execução | Smoke tests de ambiente isolado |
| Playwright | Fluxo no navegador, interface e interação | Testes 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](/guias/criando-virtual-environment/). No terminal:

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

Ative o ambiente conforme o sistema:

```bash
# Linux ou macOS
source .venv/bin/activate

# Windows PowerShell
.venv\Scripts\Activate.ps1
```

Instale as dependências:

```bash
python -m pip install Flask pytest
```

A estrutura final será:

```text
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`:

```python
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`:

```python
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`:

```python
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](/guias/testes-com-pytest/).

## 5. Cubra JSON malformado e método incorreto

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

```python
@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:

```bash
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:

```bash
python -m pytest -q -k titulo
```

Para interromper na primeira falha e mostrar detalhes:

```bash
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](/blog/uv-gerenciador-pacotes-python/) é 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](/guias/playwright-python-testes-e2e/). Para entender o próximo passo de persistência, veja [SQLAlchemy](/blog/sqlalchemy-2-orm-moderno-python/).

## 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

- [Flask: Testing Flask Applications](https://flask.palletsprojects.com/en/stable/testing/)
- [Flask: Application Factories](https://flask.palletsprojects.com/en/stable/patterns/appfactories/)
- [pytest: fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html)
- [pytest: parametrização](https://docs.pytest.org/en/stable/how-to/parametrize.html)
