Spec-Zone.ru › FastAPI

Тестирование

Благодаря Starlette тестирование приложений FastAPI — это просто и приятно.

Оно основано на HTTPX, который, в свою очередь, разработан на основе Requests, поэтому он очень знакомый и интуитивно понятный.

С его помощью вы можете использовать pytest непосредственно с FastAPI.

Использование TestClient

Примечание

Чтобы использовать TestClient, сначала установите httpx.

Добавьте его в свой проект:

$ uv add httpx

Импортируйте TestClient.

Создайте TestClient, передав ему своё приложение FastAPI.

Создайте функции, имена которых начинаются с test_ (это стандартное соглашение pytest).

Используйте объект TestClient так же, как httpx.

Пишите простые инструкции assert со стандартными выражениями Python, которые нужно проверить (опять же, стандартное соглашение pytest).

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}


client = TestClient(app)


def test_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.

Совет

Если помимо отправки запросов вашему приложению FastAPI вы хотите вызывать в тестах функции async (например, асинхронные функции базы данных), ознакомьтесь с разделом Асинхронные тесты в руководстве для продвинутых пользователей.

Разделение тестов

В реальном приложении вы, вероятно, разместите тесты в отдельном файле.

Ваше приложение FastAPI также может состоять из нескольких файлов/модулей и т. д.

Файл приложения FastAPI

Допустим, у вас такая структура файлов, как описано в разделе Большие приложения:

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

В файле main.py находится ваше приложение FastAPI:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}

Файл тестирования

Затем вы можете создать файл test_main.py с тестами. Он может находиться в том же пакете Python (в той же директории, что и файл __init__.py):

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

Поскольку этот файл находится в том же пакете, вы можете использовать относительный импорт, чтобы импортировать объект app из модуля main (main.py):

from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_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.

from typing import Annotated

from fastapi import FastAPI, Header, HTTPException
from pydantic import 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()


class Item(BaseModel):
    id: str
    title: str
    description: str | None = None


@app.get("/items/{item_id}", response_model=Item)
async def read_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 def create_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
🤓 Другие версии и варианты

Совет

По возможности предпочитайте версию Annotated.

from fastapi import FastAPI, Header, HTTPException
from pydantic import 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()


class Item(BaseModel):
    id: str
    title: str
    description: str | None = None


@app.get("/items/{item_id}", response_model=Item)
async def read_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 def create_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, добавив расширенные тесты:

from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_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",
    }


def test_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"}


def test_read_nonexistent_item():
    response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
    assert response.status_code == 404
    assert response.json() == {"detail": "Item not found"}


def test_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",
    }


def test_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"}


def test_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"}
🤓 Другие версии и варианты

Совет

По возможности предпочитайте версию Annotated.

from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_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",
    }


def test_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"}


def test_read_nonexistent_item():
    response = client.get("/items/baz", headers={"X-Token": "coneofsilence"})
    assert response.status_code == 404
    assert response.json() == {"detail": "Item not found"}


def test_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",
    }


def test_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"}


def test_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"}

Если вам нужно передать данные в запросе с помощью клиента и вы не знаете, как это сделать, поищите в интернете, как это делается в httpx или даже с помощью requests, поскольку HTTPX разработан на основе Requests.

Затем просто сделайте то же самое в тестах.

Например:

  • Чтобы передать параметр пути или запроса, добавьте его непосредственно в URL.
  • Чтобы передать тело JSON, передайте объект Python (например, dict) в параметр json.
  • Если вместо JSON нужно отправить данные формы, используйте параметр data.
  • Чтобы передать заголовки, используйте dict в параметре headers.
  • Для файлов cookie используйте dict в параметре cookies.

Дополнительную информацию о передаче данных на серверную часть (с помощью httpx или TestClient) см. в документации HTTPX.

Примечание

Обратите внимание, что TestClient получает данные, которые можно преобразовать в JSON, а не модели Pydantic.

Если в тесте у вас есть модель Pydantic и вы хотите отправить её данные приложению во время тестирования, можно использовать jsonable_encoder, описанный в разделе Кодировщик, совместимый с JSON.

Запуск

После этого вам нужно только установить pytest.

Добавьте его в свой проект:

$ uv add pytest

---> 100%

Он автоматически найдёт файлы и тесты, выполнит их и сообщит вам результаты.

Запустите тесты командой:

$ uv run 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>

© 2018 Sebastián Ramírez
Licensed under the MIT License.
https://fastapi.tiangolo.com/tutorial/testing/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API