Ir para o conteúdo

Testando

🌐 Tradução por IA e humanos

Esta tradução foi feita por IA orientada por humanos. 🤝

Ela pode conter erros de interpretação do significado original ou soar pouco natural, etc. 🤖

Você pode melhorar esta tradução ajudando-nos a orientar melhor o LLM de IA.

Versão em inglês

Graças ao Starlette, testar aplicações FastAPI é fácil e agradável.

Ele é baseado no HTTPX, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo.

Com ele, você pode usar o pytest diretamente com FastAPI.

Usando TestClient

Nota

Para usar o TestClient, primeiro instale httpx.

Certifique-se de criar um ambiente virtual, ativá-lo e instalá-lo, por exemplo:

$ pipinstallhttpx

Importe TestClient.

Crie um TestClient passando sua aplicação FastAPI para ele.

Crie funções com um nome que comece com test_ (essa é a convenção padrão do pytest).

Use o objeto TestClient da mesma forma que você faz com httpx.

Escreva instruções assert simples com as expressões Python padrão que você precisa verificar (novamente, pytest padrão).

fromfastapiimport FastAPI
fromfastapi.testclientimport TestClient
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

Dica

Observe que as funções de teste são def normais, não async def.

E as chamadas para o cliente também são chamadas normais, não usando await.

Isso permite que você use pytest diretamente sem complicações.

Detalhes Técnicos

Você também pode usar from starlette.testclient import TestClient.

FastAPI fornece o mesmo starlette.testclient que fastapi.testclient apenas como uma conveniência para você, o desenvolvedor. Mas ele vem diretamente da Starlette.

Dica

Se você quiser chamar funções async em seus testes além de enviar requests à sua aplicação FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em Testes assíncronos no tutorial avançado.

Separando testes

Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente.

E sua aplicação FastAPI também pode ser composta de vários arquivos/módulos, etc.

Arquivo da aplicação FastAPI

Digamos que você tenha uma estrutura de arquivo conforme descrito em Aplicações maiores:

.
├── app
│  ├── __init__.py
│  └── main.py

No arquivo main.py você tem sua aplicação FastAPI:

fromfastapiimport FastAPI
app = FastAPI()
@app.get("/")
async defread_main():
 return {"msg": "Hello World"}

Arquivo de teste

Então você poderia ter um arquivo test_main.py com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo __init__.py):

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto app do módulo main (main.py):

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_main():
 response = client.get("/")
 assert response.status_code == 200
 assert response.json() == {"msg": "Hello World"}

...e ter o código para os testes como antes.

Testando: exemplo estendido

Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes.

Arquivo de aplicação FastAPI estendido

Vamos continuar com a mesma estrutura de arquivo de antes:

.
├── app
│  ├── __init__.py
│  ├── main.py
│  └── test_main.py

Digamos que agora o arquivo main.py com sua aplicação FastAPI tenha algumas outras operações de rota.

Ele tem uma operação GET que pode retornar um erro.

Ele tem uma operação POST que pode retornar vários erros.

Ambas as operações de rota requerem um cabeçalho X-Token.

fromtypingimport Annotated
fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: Annotated[str, Header()]):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: Annotated[str, Header()]) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapiimport FastAPI, Header, HTTPException
frompydanticimport BaseModel
fake_secret_token = "coneofsilence"
fake_db = {
 "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"},
 "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"},
}
app = FastAPI()
classItem(BaseModel):
 id: str
 title: str
 description: str | None = None
@app.get("/items/{item_id}", response_model=Item)
async defread_main(item_id: str, x_token: str = Header()):
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item_id not in fake_db:
 raise HTTPException(status_code=404, detail="Item not found")
 return fake_db[item_id]
@app.post("/items/")
async defcreate_item(item: Item, x_token: str = Header()) -> Item:
 if x_token != fake_secret_token:
 raise HTTPException(status_code=400, detail="Invalid X-Token header")
 if item.id in fake_db:
 raise HTTPException(status_code=409, detail="Item already exists")
 fake_db[item.id] = item.model_dump()
 return item

Arquivo de teste estendido

Você pode então atualizar test_main.py com os testes estendidos:

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

fromfastapi.testclientimport TestClient
from.mainimport app
client = TestClient(app)
deftest_read_item():
 response = client.get("/items/foo", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 200
 assert response.json() == {
 "id": "foo",
 "title": "Foo",
 "description": "There goes my hero",
 }
deftest_read_item_bad_token():
 response = client.get("/items/foo", headers={"X-Token": "hailhydra"})
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_read_nonexistent_item():
 response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
 assert response.status_code == 404
 assert response.json() == {"detail": "Item not found"}
deftest_create_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"},
 )
 assert response.status_code == 200
 assert response.json() == {
 "id": "foobar",
 "title": "Foo Bar",
 "description": "The Foo Barters",
 }
deftest_create_item_bad_token():
 response = client.post(
 "/items/",
 headers={"X-Token": "hailhydra"},
 json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"},
 )
 assert response.status_code == 400
 assert response.json() == {"detail": "Invalid X-Token header"}
deftest_create_existing_item():
 response = client.post(
 "/items/",
 headers={"X-Token": "coneofsilence"},
 json={
 "id": "foo",
 "title": "The Foo ID Stealers",
 "description": "There goes my stealer",
 },
 )
 assert response.status_code == 409
 assert response.json() == {"detail": "Item already exists"}

Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no httpx, ou até mesmo como fazer isso com requests, já que o design do HTTPX é baseado no design do Requests.

Depois é só fazer o mesmo nos seus testes.

Por exemplo:

  • Para passar um parâmetro path ou query, adicione-o à própria URL.
  • Para passar um corpo JSON, passe um objeto Python (por exemplo, um dict) para o parâmetro json.
  • Se você precisar enviar Dados de Formulário em vez de JSON, use o parâmetro data.
  • Para passar headers, use um dict no parâmetro headers.
  • Para cookies, um dict no parâmetro cookies.

Para mais informações sobre como passar dados para o backend (usando httpx ou TestClient), consulte a documentação do HTTPX.

Nota

Observe que o TestClient recebe dados que podem ser convertidos para JSON, não para modelos Pydantic.

Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para a aplicação durante o teste, poderá usar o jsonable_encoder descrito em Codificador compatível com JSON.

Execute-o

Depois disso, você só precisa instalar o pytest.

Certifique-se de criar um ambiente virtual, ativá-lo e instalá-lo, por exemplo:

$ pipinstallpytest
---> 100%

Ele detectará os arquivos e os testes automaticamente, os executará e informará os resultados para você.

Execute os testes com:

$ pytest
================ test session starts ================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /home/user/code/superawesome-cli/app
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1
collected 6 items
---> 100%
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span>
<span style="color: green;">================= 1 passed in 0.03s =================</span>

AltStyle によって変換されたページ (->オリジナル) /