Spec-Zone.ru › FastAPI

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

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

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

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

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

Информация

Для использования TestClient, сначала установите httpx.

Убедитесь, что вы создали виртуальную среду, активировали её, а затем установили её, например:

$ pip install 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.

Подсказка

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

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

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

И ваше приложение 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/", response_model=Item)
async def create_item(item: Item, x_token: Annotated[str, Header()]):
    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
    return item
from typing import Annotated, Union

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: Union[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/", response_model=Item)
async def create_item(item: Item, x_token: Annotated[str, Header()]):
    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
    return item
from typing import Union

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from typing_extensions import Annotated

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: Union[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/", response_model=Item)
async def create_item(item: Item, x_token: Annotated[str, Header()]):
    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
    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/", response_model=Item)
async def create_item(item: Item, x_token: str = Header()):
    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
    return item

Подсказка

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

from typing import Union

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: Union[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/", response_model=Item)
async def create_item(item: Item, x_token: str = Header()):
    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
    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"}

Всякий раз, когда вам нужно, чтобы клиент передавал информацию в запросе, и вы не знаете как, вы можете поискать (в 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.

Убедитесь, что вы создали виртуальную среду, активировали её, а затем установили её, например:

$ pip install pytest

---> 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>
Была ли эта страница полезной?
Спасибо за ваш отзыв!
Спасибо за ваш отзыв!

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

Spec-Zone.ru

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