Skip to content

๐Ÿ”ฌ

๐Ÿ‘ ๐Ÿ’ƒ, ๐Ÿ”ฌ FastAPI ๐Ÿˆธ โฉ & ๐Ÿ˜Œ.

โšซ๏ธ โš“๏ธ ๐Ÿ”› ๐Ÿ‡ธ๐Ÿ‡ฒ, โ” ๐Ÿ”„ ๐Ÿ— โš“๏ธ ๐Ÿ”› ๐Ÿ“จ, โšซ๏ธ ๐Ÿ“ถ ๐Ÿ˜ฐ & ๐Ÿ‹๏ธ.

โฎ๏ธ โšซ๏ธ, ๐Ÿ‘† ๐Ÿ’ช โš™๏ธ โœณ ๐Ÿ”— โฎ๏ธ FastAPI.

โš™๏ธ TestClient

Info

โš™๏ธ TestClient, ๐Ÿฅ‡ โŽ httpx.

๐Ÿคถ โ“‚. pip install httpx.

๐Ÿ—„ TestClient.

โœ TestClient ๐Ÿšถโ€โ™€๏ธ ๐Ÿ‘† FastAPI ๐Ÿˆธ โšซ๏ธ.

โœ ๐Ÿ”ข โฎ๏ธ ๐Ÿ“› ๐Ÿ‘ˆ โ–ถ๏ธ โฎ๏ธ test_ (๐Ÿ‘‰ ๐Ÿฉ pytest ๐Ÿ›).

โš™๏ธ TestClient ๐ŸŽš ๐ŸŽ ๐ŸŒŒ ๐Ÿ‘† โฎ๏ธ httpx.

โœ ๐Ÿ™… assert ๐Ÿ“„ โฎ๏ธ ๐Ÿฉ ๐Ÿ ๐Ÿงฌ ๐Ÿ‘ˆ ๐Ÿ‘† ๐Ÿ’ช โœ… (๐Ÿ”„, ๐Ÿฉ 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"}

Tip

๐Ÿ‘€ ๐Ÿ‘ˆ ๐Ÿ”ฌ ๐Ÿ”ข ๐Ÿ˜ def, ๐Ÿšซ async def.

& ๐Ÿค™ ๐Ÿ‘ฉโ€๐Ÿ’ป ๐Ÿ˜ ๐Ÿค™, ๐Ÿšซ โš™๏ธ await.

๐Ÿ‘‰ โœ” ๐Ÿ‘† โš™๏ธ pytest ๐Ÿ”— ๐Ÿต ๐Ÿคข.

๐Ÿ“ก โ„น

๐Ÿ‘† ๐Ÿ’ช โš™๏ธ from starlette.testclient import TestClient.

FastAPI ๐Ÿšš ๐ŸŽ starlette.testclient fastapi.testclient ๐Ÿช ๐Ÿ‘†, ๐Ÿ‘ฉโ€๐Ÿ’ป. โœ‹๏ธ โšซ๏ธ ๐Ÿ‘Ÿ ๐Ÿ”— โšช๏ธโžก๏ธ ๐Ÿ’ƒ.

Tip

๐Ÿšฅ ๐Ÿ‘† ๐Ÿ’š ๐Ÿค™ async ๐Ÿ”ข ๐Ÿ‘† ๐Ÿ’ฏ โ†–๏ธ โšช๏ธโžก๏ธ ๐Ÿ“จ ๐Ÿ“จ ๐Ÿ‘† FastAPI ๐Ÿˆธ (โœ… ๐Ÿ” ๐Ÿ’ฝ ๐Ÿ”ข), โœ”๏ธ ๐Ÿ‘€ ๐Ÿ” ๐Ÿ’ฏ ๐Ÿง ๐Ÿ”ฐ.

๐ŸŽ ๐Ÿ’ฏ

๐ŸŽฐ ๐Ÿˆธ, ๐Ÿ‘† ๐ŸŽฒ ๐Ÿ”œ โœ”๏ธ ๐Ÿ‘† ๐Ÿ’ฏ ๐ŸŽ ๐Ÿ“.

& ๐Ÿ‘† FastAPI ๐Ÿˆธ 5๏ธโƒฃ๐Ÿ“† โœ ๐Ÿ“š ๐Ÿ“/๐Ÿ•น, โ™’๏ธ.

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 โฎ๏ธ ๐Ÿ‘† ๐Ÿ’ฏ. โšซ๏ธ ๐Ÿ’ช ๐Ÿ–– ๐Ÿ”› ๐ŸŽ ๐Ÿ ๐Ÿ“ฆ (๐ŸŽ ๐Ÿ“ โฎ๏ธ __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 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
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

โ†” ๐Ÿ”ฌ ๐Ÿ“

๐Ÿ‘† ๐Ÿ’ช โคด๏ธ โ„น 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_inexistent_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, ๐Ÿ‡ธ๐Ÿ‡ฒ ๐Ÿ”ง โš“๏ธ ๐Ÿ”› ๐Ÿ“จ' ๐Ÿ”ง.

โคด๏ธ ๐Ÿ‘† ๐ŸŽ ๐Ÿ‘† ๐Ÿ’ฏ.

๐Ÿคถ โ“‚.:

  • ๐Ÿšถโ€โ™€๏ธ โžก โš–๏ธ ๐Ÿ”ข ๐Ÿ”ข, ๐Ÿšฎ โšซ๏ธ ๐Ÿ“› โšซ๏ธ.
  • ๐Ÿšถโ€โ™€๏ธ ๐ŸŽป ๐Ÿ’ช, ๐Ÿšถโ€โ™€๏ธ ๐Ÿ ๐ŸŽš (โœ… dict) ๐Ÿ”ข json.
  • ๐Ÿšฅ ๐Ÿ‘† ๐Ÿ’ช ๐Ÿ“จ ๐Ÿ“จ ๐Ÿ’ฝ โ†ฉ๏ธ ๐ŸŽป, โš™๏ธ data ๐Ÿ”ข โ†ฉ๏ธ.
  • ๐Ÿšถโ€โ™€๏ธ ๐ŸŽš, โš™๏ธ dict headers ๐Ÿ”ข.
  • ๐Ÿช, dict cookies ๐Ÿ”ข.

๐ŸŒ– โ„น ๐Ÿ”ƒ โ” ๐Ÿšถโ€โ™€๏ธ ๐Ÿ’ฝ ๐Ÿ‘ฉโ€๐Ÿ’ป (โš™๏ธ httpx โš–๏ธ TestClient) โœ… ๐Ÿ‡ธ๐Ÿ‡ฒ ๐Ÿงพ.

Info

๐Ÿ—’ ๐Ÿ‘ˆ TestClient ๐Ÿ“จ ๐Ÿ’ฝ ๐Ÿ‘ˆ ๐Ÿ’ช ๐Ÿ—œ ๐ŸŽป, ๐Ÿšซ Pydantic ๐Ÿท.

๐Ÿšฅ ๐Ÿ‘† โœ”๏ธ Pydantic ๐Ÿท ๐Ÿ‘† ๐Ÿ’ฏ & ๐Ÿ‘† ๐Ÿ’š ๐Ÿ“จ ๐Ÿšฎ ๐Ÿ’ฝ ๐Ÿˆธ โฎ๏ธ ๐Ÿ”ฌ, ๐Ÿ‘† ๐Ÿ’ช โš™๏ธ jsonable_encoder ๐Ÿ”ฌ ๐ŸŽป ๐Ÿ”— ๐Ÿ”ข.

๐Ÿƒ โšซ๏ธ

โฎ๏ธ ๐Ÿ‘ˆ, ๐Ÿ‘† ๐Ÿ’ช โŽ 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>