Тестування¶
🌐 Переклад ШІ та людьми
Цей переклад виконано ШІ під керівництвом людей. 🤝
Можливі помилки через неправильне розуміння початкового змісту або неприродні формулювання тощо. 🤖
Ви можете покращити цей переклад, допомігши нам краще спрямовувати AI LLM.
Завдяки Starlette, тестувати застосунки FastAPI просто й приємно.
Воно базується на HTTPX, який, своєю чергою, спроєктований на основі Requests, тож він дуже знайомий та інтуїтивно зрозумілий.
З його допомогою ви можете використовувати pytest безпосередньо з FastAPI.
Використання TestClient¶
Примітка
Щоб використовувати TestClient, спочатку встановіть httpx.
Переконайтеся, що ви створили віртуальне середовище, активували його, а потім встановили httpx, наприклад:
$ pipinstallhttpx
Імпортуйте TestClient.
Створіть TestClient, передавши йому ваш застосунок FastAPI.
Створюйте функції з іменами, що починаються з test_ (це стандартна угода для pytest).
Використовуйте об'єкт TestClient так само як і httpx.
Записуйте прості assert-вирази зі стандартними виразами Python, які потрібно перевірити (це також стандарт для pytest).
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"}
Порада
Зверніть увагу, що тестові функції — це звичайні def, а не async def.
Виклики клієнта також звичайні, без використання await.
Це дозволяє використовувати pytest без зайвих ускладнень.
Технічні деталі
Ви також можете використовувати from starlette.testclient import TestClient.
FastAPI надає той самий starlette.testclient під назвою fastapi.testclient просто для зручності для вас, розробника. Але він безпосередньо походить із Starlette.
Порада
Якщо ви хочете викликати async-функції у ваших тестах, окрім відправлення запитів до вашого застосунку FastAPI (наприклад, асинхронні функції роботи з базою даних), перегляньте Асинхронні тести у просунутому навчальному посібнику.
Розділення тестів¶
У реальному застосунку ваші тести, ймовірно, будуть в окремому файлі.
Також ваш застосунок FastAPI може складатися з кількох файлів/модулів тощо.
Файл застосунку FastAPI¶
Припустимо, у вас є структура файлів, описана в розділі Більші застосунки:
.
├── app
│ ├── __init__.py
│ └── main.py
У файлі main.py знаходиться ваш застосунок FastAPI:
fromfastapiimport FastAPI
app = FastAPI()
@app.get("/")
async defread_main():
return {"msg": "Hello World"}
Файл тестування¶
Ви можете створити файл test_main.py з вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом __init__.py):
.
├── app
│ ├── __init__.py
│ ├── main.py
│ └── test_main.py
Оскільки цей файл знаходиться в тому ж пакеті, ви можете використовувати відносний імпорт, щоб імпортувати об'єкт app із модуля 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"}
...і написати код для тестів так само як і раніше.
Тестування: розширений приклад¶
Тепер розширимо цей приклад і додамо більше деталей, щоб побачити, як тестувати різні частини.
Розширений файл застосунку FastAPI¶
Залишимо ту саму структуру файлів:
.
├── app
│ ├── __init__.py
│ ├── main.py
│ └── test_main.py
Припустимо, що тепер файл main.py із вашим застосунком FastAPI містить інші операції шляху.
Він має GET-операцію, яка може повертати помилку.
Він має POST-операцію, яка може повертати кілька помилок.
Обидві операції шляху вимагають заголовок 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
Розширений тестовий файл¶
Потім ви можете оновити test_main.py, додавши розширені тести:
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"}
Коли вам потрібно, щоб клієнт передав інформацію в запиті, але ви не знаєте, як це зробити, ви можете пошукати (Google), як це зробити в httpx, або навіть як це зробити з requests, оскільки дизайн HTTPX базується на дизайні Requests.
Далі ви просто повторюєте ці ж дії у ваших тестах.
Наприклад:
- Щоб передати параметр шляху або запиту, додайте його безпосередньо до URL.
- Щоб передати тіло JSON, передайте Python-об'єкт (наприклад,
dict) у параметрjson. - Якщо потрібно надіслати дані форми замість JSON, використовуйте параметр
data. - Щоб передати заголовки, використовуйте
dictу параметріheaders. - Для кукі використовуйте
dictу параметріcookies.
Докладніше про передачу даних у бекенд (за допомогою httpx або TestClient) можна знайти в документації HTTPX.
Примітка
Зверніть увагу, що TestClient отримує дані, які можна конвертувати в JSON, а не Pydantic-моделі.
Якщо у вас є Pydantic-модель у тесті, і ви хочете передати її дані в застосунок під час тестування, ви можете використати jsonable_encoder, описаний у розділі JSON-сумісний кодувальник.
Запуск¶
Після цього вам потрібно встановити pytest.
Переконайтеся, що ви створили віртуальне середовище, активували його і встановили необхідні пакети, наприклад:
$ pipinstallpytest
---> 100%
Він автоматично знайде файли та тести, виконає їх і повідомить вам результати.
Запустіть тести за допомогою:
$ 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>