Aprenda a testar eficazmente as suas aplicações FastAPI usando o TestClient. Aborda as melhores práticas, técnicas avançadas e exemplos do mundo real para APIs robustas e confiáveis.
Dominando Testes com FastAPI: Um Guia Abrangente do TestClient
O FastAPI surgiu como um framework líder para a construção de APIs de alto desempenho com Python. A sua velocidade, facilidade de uso e validação automática de dados fazem dele um favorito entre desenvolvedores em todo o mundo. No entanto, uma API bem construída é tão boa quanto os seus testes. Testes completos garantem que a sua API funcione como esperado, permaneça estável sob pressão e possa ser implantada em produção com confiança. Este guia abrangente foca no uso do TestClient do FastAPI para testar eficazmente os seus endpoints de API.
Por Que Testar é Importante para Aplicações FastAPI?
Testar é um passo crucial no ciclo de vida do desenvolvimento de software. Ajuda você a:
- Identificar bugs precocemente: Capture erros antes que cheguem à produção, economizando tempo e recursos.
- Garantir a qualidade do código: Promova um código bem estruturado e de fácil manutenção.
- Prevenir regressões: Garanta que novas alterações não quebrem funcionalidades existentes.
- Melhorar a confiabilidade da API: Construa confiança na estabilidade e no desempenho da API.
- Facilitar a colaboração: Forneça uma documentação clara do comportamento esperado para outros desenvolvedores.
Apresentando o TestClient do FastAPI
O FastAPI fornece um TestClient integrado que simplifica o processo de teste dos seus endpoints de API. O TestClient atua como um cliente leve que pode enviar requisições para a sua API sem iniciar um servidor completo. Isso torna os testes significativamente mais rápidos e convenientes.
Principais Funcionalidades do TestClient:
- Simula requisições HTTP: Permite que você envie requisições GET, POST, PUT, DELETE e outras requisições HTTP para a sua API.
- Lida com a serialização de dados: Serializa automaticamente os dados da requisição (por exemplo, payloads JSON) e desserializa os dados da resposta.
- Fornece métodos de asserção: Oferece métodos convenientes para verificar o código de status, os cabeçalhos e o conteúdo das respostas.
- Suporta testes assíncronos: Funciona perfeitamente com a natureza assíncrona do FastAPI.
- Integra-se com frameworks de teste: Integra-se facilmente com frameworks de teste populares do Python, como pytest e unittest.
Configurando Seu Ambiente de Teste
Antes de começar a testar, você precisa configurar seu ambiente de teste. Isso normalmente envolve a instalação das dependências necessárias e a configuração do seu framework de teste.
Instalação
Primeiro, certifique-se de que você tem o FastAPI e o pytest instalados. Você pode instalá-los usando o pip:
pip install fastapi pytest httpx
httpx é um cliente HTTP que o FastAPI usa internamente. Embora o TestClient faça parte do FastAPI, ter o httpx instalado também garante testes tranquilos. Alguns tutoriais também mencionam o requests, no entanto, o httpx está mais alinhado com a natureza assíncrona do FastAPI.
Exemplo de Aplicação FastAPI
Vamos criar uma aplicação FastAPI simples que podemos usar para testes:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Salve este código como main.py. Esta aplicação define três endpoints:
/: Um endpoint GET simples que retorna uma mensagem "Hello World"./items/{item_id}: Um endpoint GET que retorna um item com base no seu ID./items/: Um endpoint POST que cria um novo item.
Escrevendo Seu Primeiro Teste
Agora que você tem uma aplicação FastAPI, pode começar a escrever testes usando o TestClient. Crie um novo arquivo chamado test_main.py no mesmo diretório que main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
Neste teste:
- Importamos o
TestCliente a instânciaappdo FastAPI. - Criamos uma instância do
TestClient, passando oapp. - Definimos uma função de teste
test_read_root. - Dentro da função de teste, usamos
client.get("/")para enviar uma requisição GET para o endpoint raiz. - Verificamos se o código de status da resposta é 200 (OK).
- Verificamos se o JSON da resposta é igual a
{"message": "Hello World"}.
Executando Seus Testes com o pytest
Para executar seus testes, basta abrir um terminal no diretório que contém seu arquivo test_main.py e executar o seguinte comando:
pytest
O pytest descobrirá e executará automaticamente todos os testes do seu projeto. Você deve ver uma saída semelhante a esta:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Testando Diferentes Métodos HTTP
O TestClient suporta todos os métodos HTTP padrão, incluindo GET, POST, PUT, DELETE e PATCH. Vamos ver como testar cada um desses métodos.
Testando Requisições GET
Já vimos um exemplo de teste de uma requisição GET na seção anterior. Aqui está outro exemplo, testando o endpoint /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Este teste envia uma requisição GET para /items/1 com um parâmetro de consulta q=test. Em seguida, verifica se o código de status da resposta é 200 e se o JSON da resposta contém os dados esperados.
Testando Requisições POST
Para testar uma requisição POST, você precisa enviar dados no corpo da requisição. O TestClient serializa automaticamente os dados para JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
Neste teste:
- Criamos um dicionário
item_datacontendo os dados para o novo item. - Usamos
client.post("/items/", json=item_data)para enviar uma requisição POST para o endpoint/items/, passandoitem_datacomo o payload JSON. - Verificamos se o código de status da resposta é 200 e se o JSON da resposta corresponde ao
item_data.
Testando Requisições PUT, DELETE e PATCH
Testar requisições PUT, DELETE e PATCH é semelhante a testar requisições POST. Você simplesmente usa os métodos correspondentes no TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Adicione asserções para a resposta esperada
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Adicione asserções para a resposta esperada
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Adicione asserções para a resposta esperada
Lembre-se de adicionar asserções para verificar se as respostas são as esperadas.
Técnicas Avançadas de Teste
O TestClient oferece várias funcionalidades avançadas que podem ajudá-lo a escrever testes mais abrangentes e eficazes.
Testando com Dependências
O sistema de injeção de dependência do FastAPI permite injetar facilmente dependências em seus endpoints de API. Ao testar, você pode querer substituir essas dependências para fornecer implementações mock ou específicas para o teste.
Por exemplo, suponha que sua aplicação dependa de uma conexão com o banco de dados. Você pode substituir a dependência do banco de dados em seus testes para usar um banco de dados em memória:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Configuração do Banco de Dados
DATABASE_URL = "sqlite:///./test.db" # Banco de dados em memória para testes
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Definir Modelo de Usuário
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# App FastAPI
app = FastAPI()
# Dependência para obter a sessão do banco de dados
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint para criar um usuário
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Substituir a dependência do banco de dados para testes
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# Primeiro, garanta que as tabelas sejam criadas, o que pode não acontecer por padrão
Base.metadata.create_all(bind=engine) # importante: crie as tabelas no banco de dados de teste
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Limpe a substituição após o teste, se necessário
app.dependency_overrides = {}
Este exemplo substitui a dependência get_db por uma função específica de teste que retorna uma sessão para um banco de dados SQLite em memória. Importante: A criação dos metadados deve ser invocada explicitamente para que o banco de dados de teste funcione corretamente. A falha na criação da tabela levará a erros relacionados à falta de tabelas.
Testando Código Assíncrono
O FastAPI é construído para ser assíncrono, então você frequentemente precisará testar código assíncrono. O TestClient suporta testes assíncronos de forma transparente.
Para testar um endpoint assíncrono, simplesmente defina sua função de teste como async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simula alguma operação assíncrona
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Necessário para ser compatível com pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Nota: Você precisa instalar o pytest-asyncio para usar @pytest.mark.asyncio: pip install pytest-asyncio. Você também precisa garantir que asyncio.get_event_loop() esteja configurado se estiver usando versões mais antigas do pytest. Se estiver usando o pytest versão 8 ou mais recente, isso pode não ser necessário.
Testando Upload de Arquivos
O FastAPI facilita o manuseio de uploads de arquivos. Para testar uploads de arquivos, você pode usar o parâmetro files dos métodos de requisição do TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
Neste teste, criamos um arquivo fictício usando io.BytesIO e o passamos para o parâmetro files. O parâmetro files aceita uma lista de tuplas, onde cada tupla contém o nome do campo, o nome do arquivo e o conteúdo do arquivo. O tipo de conteúdo é importante para o manuseio preciso pelo servidor.
Testando o Tratamento de Erros
É importante testar como sua API lida com erros. Você pode usar o TestClient para enviar requisições inválidas e verificar se a API retorna as respostas de erro corretas.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Este teste envia uma requisição GET para /items/101, que levanta uma HTTPException com um código de status 400. O teste verifica se o código de status da resposta é 400 e se o JSON da resposta contém a mensagem de erro esperada.
Testando Funcionalidades de Segurança
Se sua API usa autenticação ou autorização, você também precisará testar essas funcionalidades de segurança. O TestClient permite que você defina cabeçalhos e cookies para simular requisições autenticadas.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Segurança
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simular autenticação
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# Primeiro, obtenha um token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Em seguida, use o token para acessar a rota protegida
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # formato corrigido.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
Neste exemplo, testamos o endpoint de login e, em seguida, usamos o token obtido para acessar uma rota protegida. O parâmetro headers dos métodos de requisição do TestClient permite definir cabeçalhos personalizados, incluindo o cabeçalho Authorization para tokens bearer.
Melhores Práticas para Testes com FastAPI
Aqui estão algumas melhores práticas a serem seguidas ao testar suas aplicações FastAPI:
- Escreva testes abrangentes: Busque alta cobertura de testes para garantir que todas as partes da sua API sejam completamente testadas.
- Use nomes de teste descritivos: Certifique-se de que os nomes dos seus testes indiquem claramente o que o teste está verificando.
- Siga o padrão Arrange-Act-Assert: Organize seus testes em três fases distintas: Arrange (preparar os dados de teste), Act (executar a ação que está sendo testada) e Assert (verificar os resultados).
- Use objetos mock: Simule dependências externas para isolar seus testes e evitar depender de sistemas externos.
- Teste casos extremos (edge cases): Teste sua API com entradas inválidas ou inesperadas para garantir que ela lide com erros de forma elegante.
- Execute os testes com frequência: Integre os testes ao seu fluxo de trabalho de desenvolvimento para detectar bugs cedo e com frequência.
- Integre com CI/CD: Automatize seus testes em seu pipeline de CI/CD para garantir que todas as alterações de código sejam completamente testadas antes de serem implantadas em produção. Ferramentas como Jenkins, GitLab CI, GitHub Actions ou CircleCI podem ser usadas para alcançar isso.
Exemplo: Teste de Internacionalização (i18n)
Ao desenvolver APIs para um público global, a internacionalização (i18n) é essencial. Testar a i18n envolve verificar se sua API suporta múltiplos idiomas e regiões corretamente. Aqui está um exemplo de como você pode testar a i18n em uma aplicação FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Este exemplo define o cabeçalho Accept-Language para especificar o idioma desejado. A API retorna a saudação no idioma especificado. Os testes garantem que a API lide corretamente com diferentes preferências de idioma. Se o cabeçalho Accept-Language estiver ausente, o idioma padrão "en" é usado.
Conclusão
Testar é uma parte essencial da construção de aplicações FastAPI robustas e confiáveis. O TestClient fornece uma maneira simples e conveniente de testar seus endpoints de API. Seguindo as melhores práticas descritas neste guia, você pode escrever testes abrangentes que garantem a qualidade e a estabilidade de suas APIs. Desde requisições básicas até técnicas avançadas como injeção de dependência e testes assíncronos, o TestClient capacita você a criar código bem testado e de fácil manutenção. Adote os testes como parte central do seu fluxo de desenvolvimento, e você construirá APIs que são tanto poderosas quanto confiáveis para usuários em todo o mundo. Lembre-se da importância da integração com CI/CD para automatizar os testes e garantir a garantia de qualidade contínua.