Научете как ефективно да тествате вашите FastAPI приложения, използвайки TestClient. Обхваща най-добрите практики, разширени техники и примери от реалния свят за надеждни API.
Овладяване на тестването във FastAPI: Изчерпателно ръководство за TestClient
FastAPI се очерта като водеща рамка за изграждане на високопроизводителни API с Python. Неговата скорост, лекота на използване и автоматична валидация на данни го правят любимец сред разработчиците по целия свят. Въпреки това, добре изграденият API е толкова добър, колкото и неговите тестове. Задълбоченото тестване гарантира, че вашият API функционира според очакванията, остава стабилен под напрежение и може да бъде разгърнат уверено в производствена среда. Това изчерпателно ръководство се фокусира върху използването на TestClient на FastAPI за ефективно тестване на вашите API крайни точки.
Защо тестването е важно за FastAPI приложенията?
Тестването е решаваща стъпка в жизнения цикъл на разработка на софтуер. То ви помага да:
- Идентифицирате грешки рано: Откривате грешки, преди те да достигнат до производствена среда, спестявайки време и ресурси.
- Осигурите качество на кода: Насърчавате добре структуриран и поддържащ се код.
- Предотвратите регресии: Гарантирате, че новите промени не нарушават съществуващата функционалност.
- Подобрите надеждността на API: Изграждате увереност в стабилността и производителността на API.
- Улесните сътрудничеството: Предоставяте ясна документация за очакваното поведение за други разработчици.
Представяне на TestClient на FastAPI
FastAPI предоставя вграден TestClient, който опростява процеса на тестване на вашите API крайни точки. TestClient действа като лек клиент, който може да изпраща заявки към вашия API, без да стартира пълноценен сървър. Това прави тестването значително по-бързо и по-удобно.
Основни характеристики на TestClient:
- Симулира HTTP заявки: Позволява ви да изпращате GET, POST, PUT, DELETE и други HTTP заявки към вашия API.
- Обработва сериализация на данни: Автоматично сериализира данните от заявката (напр. JSON полезни данни) и десериализира данните от отговора.
- Предоставя методи за проверка: Предлага удобни методи за проверка на кода на състоянието, заглавките и съдържанието на отговорите.
- Поддържа асинхронно тестване: Работи безпроблемно с асинхронната природа на FastAPI.
- Интегрира се с рамки за тестване: Лесно се интегрира с популярни Python рамки за тестване като pytest и unittest.
Настройка на вашата среда за тестване
Преди да започнете да тествате, трябва да настроите вашата среда за тестване. Това обикновено включва инсталиране на необходимите зависимости и конфигуриране на вашата рамка за тестване.
Инсталация
Първо, уверете се, че имате инсталирани FastAPI и pytest. Можете да ги инсталирате с помощта на pip:
pip install fastapi pytest httpx
httpx е HTTP клиент, който FastAPI използва под капака. Въпреки че TestClient е част от FastAPI, инсталирането на httpx също така гарантира гладко тестване. Някои уроци също споменават requests, но httpx е по-съвместим с асинхронната природа на FastAPI.
Примерен FastAPI Application
Нека създадем просто FastAPI приложение, което можем да използваме за тестване:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Запазете този код като main.py. Това приложение дефинира три крайни точки:
/: Проста GET крайна точка, която връща съобщение "Hello World"./items/{item_id}: GET крайна точка, която връща елемент въз основа на неговия ID./items/: POST крайна точка, която създава нов елемент.
Написване на първия ви тест
Сега, след като имате FastAPI приложение, можете да започнете да пишете тестове, използвайки TestClient. Създайте нов файл с име test_main.py в същата директория като main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
В този тест:
- Импортираме
TestClientи FastAPIappинстанцията. - Създаваме инстанция на
TestClient, предавайкиapp. - Дефинираме тестова функция
test_read_root. - Вътре в тестовата функция използваме
client.get("/"), за да изпратим GET заявка до коренната крайна точка. - Уверяваме се, че кодът на състоянието на отговора е 200 (OK).
- Уверяваме се, че JSON отговорът е равен на
{"message": "Hello World"}.
Изпълнение на вашите тестове с pytest
За да изпълните тестовете си, просто отворете терминал в директорията, съдържаща вашия test_main.py файл, и изпълнете следната команда:
pytest
pytest автоматично ще открие и изпълни всички тестове във вашия проект. Трябва да видите резултат, подобен на този:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Тестване на различни HTTP методи
TestClient поддържа всички стандартни HTTP методи, включително GET, POST, PUT, DELETE и PATCH. Нека видим как да тестваме всеки от тези методи.
Тестване на GET заявки
Вече видяхме пример за тестване на GET заявка в предишния раздел. Ето още един пример, тестващ крайната точка /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Този тест изпраща GET заявка до /items/1 с параметър на заявка q=test. След това се уверява, че кодът на състоянието на отговора е 200 и че JSON отговорът съдържа очакваните данни.
Тестване на POST заявки
За да тествате POST заявка, трябва да изпратите данни в тялото на заявката. TestClient автоматично сериализира данните в JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
В този тест:
- Създаваме речник
item_data, съдържащ данните за новия елемент. - Използваме
client.post("/items/", json=item_data), за да изпратим POST заявка до крайната точка/items/, предавайкиitem_dataкато JSON полезен товар. - Уверяваме се, че кодът на състоянието на отговора е 200 и че JSON отговорът съвпада с
item_data.
Тестване на PUT, DELETE и PATCH заявки
Тестването на PUT, DELETE и PATCH заявки е подобно на тестването на POST заявки. Просто използвате съответните методи на TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
Не забравяйте да добавите проверки, за да проверите дали отговорите са според очакванията.
Разширени техники за тестване
TestClient предлага няколко разширени функции, които могат да ви помогнат да пишете по-изчерпателни и ефективни тестове.
Тестване със зависимости
Системата за инжектиране на зависимости на FastAPI ви позволява лесно да инжектирате зависимости във вашите API крайни точки. Когато тествате, може да искате да замените тези зависимости, за да предоставите макети или специфични за теста реализации.
Например, да предположим, че приложението ви зависи от връзка с база данни. Можете да замените зависимостта от базата данни във вашите тестове, за да използвате база данни в паметта:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
Този пример заменя зависимостта get_db със специфична за теста функция, която връща сесия към SQLite база данни в паметта. Важно: Създаването на метаданни трябва да бъде изрично извикано, за да може тестовата база данни да функционира правилно. Ако не успеете да създадете таблицата, ще доведе до грешки, свързани с липсващи таблици.
Тестване на асинхронен код
FastAPI е създаден да бъде асинхронен, така че често ще трябва да тествате асинхронен код. TestClient поддържа безпроблемно асинхронно тестване.
За да тествате асинхронна крайна точка, просто дефинирайте тестовата си функция като async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Забележка: Трябва да инсталирате pytest-asyncio, за да използвате @pytest.mark.asyncio: pip install pytest-asyncio. Трябва също да се уверите, че asyncio.get_event_loop() е конфигуриран, ако използвате по-стари версии на pytest. Ако използвате pytest версия 8 или по-нова, това може да не е необходимо.
Тестване на качване на файлове
FastAPI улеснява обработката на качване на файлове. За да тествате качване на файлове, можете да използвате параметъра files на методите за заявка на TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
В този тест създаваме фиктивен файл с помощта на io.BytesIO и го предаваме на параметъра files. Параметърът files приема списък с кортежи, където всеки кортеж съдържа името на полето, името на файла и съдържанието на файла. Типът на съдържанието е важен за точното обработване от сървъра.
Тестване на обработка на грешки
Важно е да тествате как вашият API обработва грешки. Можете да използвате TestClient, за да изпращате невалидни заявки и да проверите дали API връща правилните отговори за грешки.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Този тест изпраща GET заявка до /items/101, която генерира HTTPException с код на състоянието 400. Тестът проверява дали кодът на състоянието на отговора е 400 и че JSON отговорът съдържа очакваното съобщение за грешка.
Тестване на функции за сигурност
Ако вашият API използва удостоверяване или оторизация, ще трябва да тествате и тези функции за сигурност. TestClient ви позволява да задавате заглавки и бисквитки, за да симулирате удостоверени заявки.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
В този пример тестваме крайната точка за влизане и след това използваме получения токен за достъп до защитен маршрут. Параметърът headers на методите за заявка на TestClient ви позволява да задавате персонализирани заглавки, включително заглавката Authorization за токени на носители.
Най-добри практики за тестване на FastAPI
Ето някои най-добри практики, които трябва да следвате, когато тествате вашите FastAPI приложения:
- Пишете изчерпателни тестове: Стремете се към високо покритие на тестовете, за да сте сигурни, че всички части на вашия API са задълбочено тествани.
- Използвайте описателни имена на тестове: Уверете се, че имената на вашите тестове ясно посочват какво проверява тестът.
- Следвайте шаблона Arrange-Act-Assert: Организирайте тестовете си в три отделни фази: Arrange (настройка на тестовите данни), Act (извършване на действието, което се тества) и Assert (проверка на резултатите).
- Използвайте макети: Макетирайте външни зависимости, за да изолирате тестовете си и да избегнете разчитането на външни системи.
- Тествайте гранични случаи: Тествайте вашия API с невалиден или неочакван вход, за да сте сигурни, че той обработва грешките грациозно.
- Изпълнявайте тестове често: Интегрирайте тестването във вашия работен процес за разработка, за да хващате грешки рано и често.
- Интегрирайте се с CI/CD: Автоматизирайте вашите тестове във вашата CI/CD тръба, за да сте сигурни, че всички промени в кода са задълбочено тествани, преди да бъдат разгърнати в производствена среда. Инструменти като Jenkins, GitLab CI, GitHub Actions или CircleCI могат да бъдат използвани за постигане на това.
Пример: Тестване на интернационализация (i18n)
Когато разработвате API за глобална аудитория, интернационализацията (i18n) е от съществено значение. Тестването на i18n включва проверка дали вашият API поддържа правилно множество езици и региони. Ето пример за това как можете да тествате i18n в FastAPI приложение:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Този пример задава заглавката Accept-Language, за да укаже желания език. API връща поздрав на указания език. Тестването гарантира, че API правилно обработва различни езикови предпочитания. Ако заглавката Accept-Language липсва, се използва езикът по подразбиране "en".
Заключение
Тестването е съществена част от изграждането на здрави и надеждни FastAPI приложения. TestClient предоставя прост и удобен начин за тестване на вашите API крайни точки. Следвайки най-добрите практики, описани в това ръководство, можете да пишете изчерпателни тестове, които да гарантират качеството и стабилността на вашите API. От основни заявки до разширени техники като инжектиране на зависимости и асинхронно тестване, TestClient ви дава възможност да създавате добре тестван и поддържащ се код. Прегърнете тестването като основна част от вашия работен процес за разработка и ще изградите API, които са едновременно мощни и надеждни за потребителите по целия свят. Не забравяйте важността на CI/CD интеграцията за автоматизиране на тестването и осигуряване на непрекъснато осигуряване на качеството.