Học cách kiểm thử ứng dụng FastAPI hiệu quả với TestClient. Khám phá các phương pháp hay nhất, kỹ thuật nâng cao và ví dụ thực tế cho các API mạnh mẽ, đáng tin cậy.
Làm chủ kiểm thử FastAPI: Hướng dẫn toàn diện về TestClient
\n\nFastAPI đã nổi lên như một framework hàng đầu để xây dựng các API hiệu suất cao bằng Python. Tốc độ, dễ sử dụng và tự động xác thực dữ liệu của nó khiến nó trở thành lựa chọn yêu thích của các nhà phát triển trên toàn thế giới. Tuy nhiên, một API được xây dựng tốt chỉ tốt bằng các bài kiểm thử của nó. Kiểm thử kỹ lưỡng đảm bảo rằng API của bạn hoạt động như mong đợi, duy trì ổn định dưới áp lực và có thể được triển khai tự tin vào môi trường sản xuất. Hướng dẫn toàn diện này tập trung vào việc sử dụng TestClient của FastAPI để kiểm thử hiệu quả các endpoint API của bạn.
Tại sao kiểm thử lại quan trọng đối với các ứng dụng FastAPI?
\n\nKiểm thử là một bước quan trọng trong vòng đời phát triển phần mềm. Nó giúp bạn:
\n\n- \n
- Xác định lỗi sớm: Phát hiện lỗi trước khi chúng đến môi trường sản xuất, tiết kiệm thời gian và tài nguyên. \n
- Đảm bảo chất lượng mã: Thúc đẩy mã có cấu trúc tốt và dễ bảo trì. \n
- Ngăn chặn hồi quy: Đảm bảo rằng các thay đổi mới không làm hỏng chức năng hiện có. \n
- Cải thiện độ tin cậy của API: Xây dựng niềm tin vào sự ổn định và hiệu suất của API. \n
- Tạo điều kiện hợp tác: Cung cấp tài liệu rõ ràng về hành vi mong đợi cho các nhà phát triển khác. \n
Giới thiệu TestClient của FastAPI
\n\nFastAPI cung cấp một TestClient tích hợp giúp đơn giản hóa quá trình kiểm thử các endpoint API của bạn. TestClient hoạt động như một client nhẹ có thể gửi yêu cầu đến API của bạn mà không cần khởi động một máy chủ đầy đủ. Điều này giúp việc kiểm thử nhanh hơn và thuận tiện hơn đáng kể.
Các tính năng chính của TestClient:
\n\n- \n
- Mô phỏng các yêu cầu HTTP: Cho phép bạn gửi các yêu cầu HTTP GET, POST, PUT, DELETE và các yêu cầu khác đến API của bạn. \n
- Xử lý tuần tự hóa dữ liệu: Tự động tuần tự hóa dữ liệu yêu cầu (ví dụ: các payload JSON) và giải tuần tự hóa dữ liệu phản hồi. \n
- Cung cấp các phương thức xác nhận: Cung cấp các phương thức tiện lợi để xác minh mã trạng thái, tiêu đề và nội dung của các phản hồi. \n
- Hỗ trợ kiểm thử bất đồng bộ: Hoạt động liền mạch với bản chất bất đồng bộ của FastAPI. \n
- Tích hợp với các framework kiểm thử: Dễ dàng tích hợp với các framework kiểm thử Python phổ biến như pytest và unittest. \n
Thiết lập môi trường kiểm thử của bạn
\n\nTrước khi bắt đầu kiểm thử, bạn cần thiết lập môi trường kiểm thử của mình. Điều này thường bao gồm việc cài đặt các dependencies cần thiết và cấu hình framework kiểm thử của bạn.
\n\nCài đặt
\n\nTrước tiên, hãy đảm bảo bạn đã cài đặt FastAPI và pytest. Bạn có thể cài đặt chúng bằng pip:
pip install fastapi pytest httpx
httpx là một HTTP client mà FastAPI sử dụng bên trong. Mặc dù TestClient là một phần của FastAPI, việc cài đặt thêm httpx đảm bảo quá trình kiểm thử diễn ra suôn sẻ. Một số hướng dẫn cũng đề cập đến requests, tuy nhiên, httpx phù hợp hơn với bản chất bất đồng bộ của FastAPI.
Ứng dụng FastAPI ví dụ
\n\nHãy tạo một ứng dụng FastAPI đơn giản mà chúng ta có thể sử dụng để kiểm thử:
\n\n
from fastapi import FastAPI\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\nclass Item(BaseModel:\n name: str\n description: str | None = None\n price: float\n tax: float | None = None\n\n@app.get("/")\nasync def read_root():\n return {"message": "Hello World"}\n\n@app.get("/items/{item_id}")\nasync def read_item(item_id: int, q: str | None = None):\n return {"item_id": item_id, "q": q}\n\n@app.post("/items/")\nasync def create_item(item: Item):\n return item\n
Lưu mã này dưới dạng main.py. Ứng dụng này định nghĩa ba endpoint:
- \n
/: Một endpoint GET đơn giản trả về thông báo "Hello World". \n /items/{item_id}: Một endpoint GET trả về một item dựa trên ID của nó. \n /items/: Một endpoint POST tạo một item mới. \n
Viết bài kiểm thử đầu tiên của bạn
\n\nBây giờ bạn đã có một ứng dụng FastAPI, bạn có thể bắt đầu viết các bài kiểm thử bằng cách sử dụng TestClient. Tạo một tệp mới có tên test_main.py trong cùng thư mục với main.py.
from fastapi.testclient import TestClient\nfrom .main import app\n\nclient = TestClient(app)\n\ndef test_read_root():\n response = client.get("/")\n assert response.status_code == 200\n assert response.json() == {"message": "Hello World"}\n
Trong bài kiểm thử này:
\n\n- \n
- Chúng ta import
TestClientvà instanceappcủa FastAPI. \n - Chúng ta tạo một instance của
TestClient, truyền vàoapp. \n - Chúng ta định nghĩa một hàm kiểm thử
test_read_root. \n - Bên trong hàm kiểm thử, chúng ta sử dụng
client.get("/")để gửi yêu cầu GET đến endpoint gốc. \n - Chúng ta xác nhận rằng mã trạng thái phản hồi là 200 (OK). \n
- Chúng ta xác nhận rằng JSON phản hồi bằng với
{"message": "Hello World"}. \n
Chạy các bài kiểm thử của bạn với pytest
\n\nĐể chạy các bài kiểm thử của bạn, chỉ cần mở một terminal trong thư mục chứa tệp test_main.py của bạn và chạy lệnh sau:
pytest
pytest sẽ tự động phát hiện và chạy tất cả các bài kiểm thử trong dự án của bạn. Bạn sẽ thấy đầu ra tương tự như sau:
\n\n
============================= test session starts ==============================\nplatform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0\nrootdir: /path/to/your/project\ncollected 1 item\n\ntest_main.py .\n\n============================== 1 passed in 0.01s ===============================
Kiểm thử các phương thức HTTP khác nhau
\n\nTestClient hỗ trợ tất cả các phương thức HTTP tiêu chuẩn, bao gồm GET, POST, PUT, DELETE và PATCH. Hãy cùng xem cách kiểm thử từng phương thức này.
Kiểm thử các yêu cầu GET
\n\nChúng ta đã thấy một ví dụ về kiểm thử yêu cầu GET trong phần trước. Đây là một ví dụ khác, kiểm thử endpoint /items/{item_id}:
def test_read_item():\n response = client.get("/items/1?q=test")\n assert response.status_code == 200\n assert response.json() == {"item_id": 1, "q": "test"}\n
Bài kiểm thử này gửi yêu cầu GET đến /items/1 với tham số truy vấn q=test. Sau đó, nó xác nhận rằng mã trạng thái phản hồi là 200 và JSON phản hồi chứa dữ liệu mong đợi.
Kiểm thử các yêu cầu POST
\n\nĐể kiểm thử yêu cầu POST, bạn cần gửi dữ liệu trong phần thân yêu cầu. TestClient tự động tuần tự hóa dữ liệu thành JSON.
def test_create_item():\n item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}\n response = client.post("/items/", json=item_data)\n assert response.status_code == 200\n assert response.json() == item_data\n
Trong bài kiểm thử này:
\n\n- \n
- Chúng ta tạo một dictionary
item_datachứa dữ liệu cho item mới. \n - Chúng ta sử dụng
client.post("/items/", json=item_data)để gửi yêu cầu POST đến endpoint/items/, truyềnitem_datalàm payload JSON. \n - Chúng ta xác nhận rằng mã trạng thái phản hồi là 200 và JSON phản hồi khớp với
item_data. \n
Kiểm thử các yêu cầu PUT, DELETE và PATCH
\n\nKiểm thử các yêu cầu PUT, DELETE và PATCH tương tự như kiểm thử các yêu cầu POST. Bạn chỉ cần sử dụng các phương thức tương ứng trên TestClient:
def test_update_item():\n item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}\n response = client.put("/items/1", json=item_data)\n assert response.status_code == 200\n # Add assertions for the expected response\n\ndef test_delete_item():\n response = client.delete("/items/1")\n assert response.status_code == 200\n # Add assertions for the expected response\n\ndef test_patch_item():\n item_data = {"price": 29.99}\n response = client.patch("/items/1", json=item_data)\n assert response.status_code == 200\n # Add assertions for the expected response
Hãy nhớ thêm các xác nhận để kiểm tra rằng các phản hồi như mong đợi.
\n\nCác kỹ thuật kiểm thử nâng cao
\n\nTestClient cung cấp một số tính năng nâng cao có thể giúp bạn viết các bài kiểm thử toàn diện và hiệu quả hơn.
Kiểm thử với các Dependencies
\n\nHệ thống dependency injection của FastAPI cho phép bạn dễ dàng inject các dependency vào các endpoint API của bạn. Khi kiểm thử, bạn có thể muốn ghi đè các dependency này để cung cấp các triển khai mock hoặc cụ thể cho kiểm thử.
\n\nVí dụ, giả sử ứng dụng của bạn phụ thuộc vào kết nối cơ sở dữ liệu. Bạn có thể ghi đè dependency cơ sở dữ liệu trong các bài kiểm thử của mình để sử dụng cơ sở dữ liệu trong bộ nhớ:
\n\n
from typing import Annotated\n\nfrom fastapi import Depends, FastAPI, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm\nfrom sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.orm import sessionmaker, declarative_base, Session\n\n# Database Configuration\nDATABASE_URL = "sqlite:///./test.db" # In-memory database for testing\nengine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n# Define User Model\nclass User(Base):\n __tablename__ = "users"\n\n id = Column(Integer, primary_key=True, index=True)\n username = Column(String, unique=True, index=True)\n password = Column(String)\n\nBase.metadata.create_all(bind=engine)\n\n# FastAPI App\napp = FastAPI()\n\n# Dependency to get the database session\ndef get_db():\n db = TestingSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n# Endpoint to create a user\n@app.post("/users/")\nasync def create_user(username: str, password: str, db: Session = Depends(get_db)):\n db_user = User(username=username, password=password)\n db.add(db_user)\n db.commit()\n db.refresh(db_user)\n return db_user\n
from fastapi.testclient import TestClient\nfrom .main import app, get_db, Base, engine, TestingSessionLocal\n\nclient = TestClient(app)\n\n# Override the database dependency for testing\n\ndef override_get_db():\n try:\n db = TestingSessionLocal()\n yield db\n finally:\n db.close()\n\napp.dependency_overrides[get_db] = override_get_db\n\ndef test_create_user():\n # First, ensure the tables are created, which may not happen by default\n Base.metadata.create_all(bind=engine) # important: create the tables in the test db\n response = client.post("/users/", params={"username": "testuser", "password": "password123"})\n assert response.status_code == 200\n assert response.json()["username"] == "testuser"\n\n # Clean up the override after the test if needed\napp.dependency_overrides = {}\n
Ví dụ này ghi đè dependency get_db bằng một hàm cụ thể cho kiểm thử, trả về một phiên cho cơ sở dữ liệu SQLite trong bộ nhớ. Quan trọng: Việc tạo metadata phải được gọi một cách rõ ràng để cơ sở dữ liệu kiểm thử hoạt động chính xác. Việc không tạo bảng sẽ dẫn đến lỗi liên quan đến các bảng bị thiếu.
Kiểm thử mã bất đồng bộ
\n\nFastAPI được xây dựng để bất đồng bộ, vì vậy bạn sẽ thường cần kiểm thử mã bất đồng bộ. TestClient hỗ trợ kiểm thử bất đồng bộ một cách liền mạch.
Để kiểm thử một endpoint bất đồng bộ, chỉ cần định nghĩa hàm kiểm thử của bạn là async:
import asyncio\n\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get("/async")\nasync def async_endpoint():\n await asyncio.sleep(0.1) # Simulate some async operation\n return {"message": "Async Hello"}\n
import pytest\nfrom fastapi.testclient import TestClient\nfrom .main import app\n\nclient = TestClient(app)\n\n@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio\nasync def test_async_endpoint():\n response = client.get("/async")\n assert response.status_code == 200\n assert response.json() == {"message": "Async Hello"}\n
Lưu ý: Bạn cần cài đặt pytest-asyncio để sử dụng @pytest.mark.asyncio: pip install pytest-asyncio. Bạn cũng cần đảm bảo asyncio.get_event_loop() được cấu hình nếu sử dụng các phiên bản pytest cũ hơn. Nếu sử dụng pytest phiên bản 8 trở lên, điều này có thể không bắt buộc.
Kiểm thử tải tệp lên
\n\nFastAPI giúp dễ dàng xử lý việc tải tệp lên. Để kiểm thử tải tệp lên, bạn có thể sử dụng tham số files của các phương thức yêu cầu của TestClient.
from fastapi import FastAPI, File, UploadFile\nfrom typing import List\n\napp = FastAPI()\n\n@app.post("/files/")\nasync def create_files(files: List[bytes] = File()):\n return {"file_sizes": [len(file) for file in files]}\n\n@app.post("/uploadfiles/")\nasync def create_upload_files(files: List[UploadFile]):\n return {"filenames": [file.filename for file in files]}\n
from fastapi.testclient import TestClient\nfrom .main import app\nimport io\n\nclient = TestClient(app)\n\ndef test_create_files():\n file_content = b"Test file content"\n files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]\n response = client.post("/files/", files=files)\n assert response.status_code == 200\n assert response.json() == {"file_sizes": [len(file_content)]}\n\ndef test_create_upload_files():\n file_content = b"Test upload file content"\n files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]\n response = client.post("/uploadfiles/", files=files)\n assert response.status_code == 200\n assert response.json() == {"filenames": ["test_upload.txt"]}\n
Trong bài kiểm thử này, chúng ta tạo một tệp giả bằng io.BytesIO và truyền nó cho tham số files. Tham số files chấp nhận một danh sách các tuple, trong đó mỗi tuple chứa tên trường, tên tệp và nội dung tệp. Kiểu nội dung quan trọng để máy chủ xử lý chính xác.
Kiểm thử xử lý lỗi
\n\nĐiều quan trọng là phải kiểm thử cách API của bạn xử lý lỗi. Bạn có thể sử dụng TestClient để gửi các yêu cầu không hợp lệ và xác minh rằng API trả về các phản hồi lỗi chính xác.
from fastapi import FastAPI, HTTPException\n\napp = FastAPI()\n\n@app.get("/items/{item_id}")\nasync def read_item(item_id: int):\n if item_id > 100:\n raise HTTPException(status_code=400, detail="Item ID too large")\n return {"item_id": item_id}\n
from fastapi.testclient import TestClient\nfrom .main import app\n\nclient = TestClient(app)\n\ndef test_read_item_error():\n response = client.get("/items/101")\n assert response.status_code == 400\n assert response.json() == {"detail": "Item ID too large"}\n
Bài kiểm thử này gửi yêu cầu GET đến /items/101, điều này gây ra một HTTPException với mã trạng thái là 400. Bài kiểm thử xác nhận rằng mã trạng thái phản hồi là 400 và JSON phản hồi chứa thông báo lỗi mong đợi.
Kiểm thử các tính năng bảo mật
\n\nNếu API của bạn sử dụng xác thực hoặc ủy quyền, bạn cũng sẽ cần kiểm thử các tính năng bảo mật này. TestClient cho phép bạn đặt tiêu đề và cookie để mô phỏng các yêu cầu đã được xác thực.
from fastapi import FastAPI, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm\n\napp = FastAPI()\n\n# Security\noauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")\n\n@app.post("/token")\nasync def login(form_data: OAuth2PasswordRequestForm = Depends()):\n # Simulate authentication\n if form_data.username != "testuser" or form_data.password != "password123":\n raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")\n return {"access_token": "fake_token", "token_type": "bearer"}\n\n@app.get("/protected")\nasync def protected_route(token: str = Depends(oauth2_scheme)):\n return {"message": "Protected data"}\n
from fastapi.testclient import TestClient\nfrom .main import app\n\nclient = TestClient(app)\n\ndef test_login():\n response = client.post("/token", data={"username": "testuser", "password": "password123"})\n assert response.status_code == 200\n assert "access_token" in response.json()\n\ndef test_protected_route():\n # First, get a token\n token_response = client.post("/token", data={"username": "testuser", "password": "password123"})\n token = token_response.json()["access_token"]\n\n # Then, use the token to access the protected route\n response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.\n assert response.status_code == 200\n assert response.json() == {"message": "Protected data"}\n
Trong ví dụ này, chúng ta kiểm thử endpoint đăng nhập và sau đó sử dụng token đã nhận được để truy cập một route được bảo vệ. Tham số headers của các phương thức yêu cầu của TestClient cho phép bạn đặt các tiêu đề tùy chỉnh, bao gồm tiêu đề Authorization cho các bearer token.
Các phương pháp hay nhất để kiểm thử FastAPI
\n\nDưới đây là một số phương pháp hay nhất cần tuân theo khi kiểm thử các ứng dụng FastAPI của bạn:
\n\n- \n
- Viết các bài kiểm thử toàn diện: Đặt mục tiêu đạt độ bao phủ kiểm thử cao để đảm bảo rằng tất cả các phần của API của bạn được kiểm thử kỹ lưỡng. \n
- Sử dụng tên kiểm thử mô tả: Đảm bảo tên kiểm thử của bạn chỉ rõ những gì bài kiểm thử đang xác minh. \n
- Tuân theo mô hình Arrange-Act-Assert: Tổ chức các bài kiểm thử của bạn thành ba giai đoạn riêng biệt: Arrange (thiết lập dữ liệu kiểm thử), Act (thực hiện hành động đang được kiểm thử) và Assert (xác minh kết quả). \n
- Sử dụng các đối tượng mock: Mock các dependency bên ngoài để cô lập các bài kiểm thử của bạn và tránh phụ thuộc vào các hệ thống bên ngoài. \n
- Kiểm thử các trường hợp biên: Kiểm thử API của bạn với đầu vào không hợp lệ hoặc không mong đợi để đảm bảo rằng nó xử lý lỗi một cách duyên dáng. \n
- Chạy kiểm thử thường xuyên: Tích hợp kiểm thử vào quy trình làm việc phát triển của bạn để phát hiện lỗi sớm và thường xuyên. \n
- Tích hợp với CI/CD: Tự động hóa các bài kiểm thử của bạn trong pipeline CI/CD để đảm bảo rằng tất cả các thay đổi mã được kiểm thử kỹ lưỡng trước khi được triển khai vào môi trường sản xuất. Các công cụ như Jenkins, GitLab CI, GitHub Actions hoặc CircleCI có thể được sử dụng để đạt được điều này. \n
Ví dụ: Kiểm thử Quốc tế hóa (i18n)
\n\nKhi phát triển API cho đối tượng toàn cầu, quốc tế hóa (i18n) là điều cần thiết. Kiểm thử i18n bao gồm việc xác minh rằng API của bạn hỗ trợ nhiều ngôn ngữ và khu vực một cách chính xác. Dưới đây là một ví dụ về cách bạn có thể kiểm thử i18n trong một ứng dụng FastAPI:
\n\n
from fastapi import FastAPI, Header\nfrom typing import Optional\n\napp = FastAPI()\n\nmessages = {\n "en": {"greeting": "Hello, world!"},\n "fr": {"greeting": "Bonjour le monde !"},\n "es": {"greeting": "¡Hola Mundo!"},\n}\n\n@app.get("/")\nasync def read_root(accept_language: Optional[str] = Header(None)):\n lang = accept_language[:2] if accept_language else "en"\n if lang not in messages:\n lang = "en"\n return {"message": messages[lang]["greeting"]}\n
from fastapi.testclient import TestClient\nfrom .main import app\n\nclient = TestClient(app)\n\ndef test_read_root_en():\n response = client.get("/", headers={"Accept-Language": "en-US"})\n assert response.status_code == 200\n assert response.json() == {"message": "Hello, world!"}\n\ndef test_read_root_fr():\n response = client.get("/", headers={"Accept-Language": "fr-FR"})\n assert response.status_code == 200\n assert response.json() == {"message": "Bonjour le monde !"}\n\ndef test_read_root_es():\n response = client.get("/", headers={"Accept-Language": "es-ES"})\n assert response.status_code == 200\n assert response.json() == {"message": "¡Hola Mundo!"}\n\ndef test_read_root_default():\n response = client.get("/")\n assert response.status_code == 200\n assert response.json() == {"message": "Hello, world!"}\n
Ví dụ này đặt tiêu đề Accept-Language để chỉ định ngôn ngữ mong muốn. API trả về lời chào bằng ngôn ngữ được chỉ định. Kiểm thử đảm bảo rằng API xử lý đúng các tùy chọn ngôn ngữ khác nhau. Nếu tiêu đề Accept-Language vắng mặt, ngôn ngữ "en" mặc định sẽ được sử dụng.
Kết luận
\n\nKiểm thử là một phần thiết yếu để xây dựng các ứng dụng FastAPI mạnh mẽ và đáng tin cậy. TestClient cung cấp một cách đơn giản và tiện lợi để kiểm thử các endpoint API của bạn. Bằng cách tuân theo các phương pháp hay nhất được nêu trong hướng dẫn này, bạn có thể viết các bài kiểm thử toàn diện, đảm bảo chất lượng và sự ổn định của các API của bạn. Từ các yêu cầu cơ bản đến các kỹ thuật nâng cao như dependency injection và kiểm thử bất đồng bộ, TestClient trao quyền cho bạn để tạo ra mã được kiểm thử tốt và dễ bảo trì. Hãy coi kiểm thử là một phần cốt lõi của quy trình làm việc phát triển của bạn, và bạn sẽ xây dựng các API vừa mạnh mẽ vừa đáng tin cậy cho người dùng trên toàn cầu. Hãy nhớ tầm quan trọng của việc tích hợp CI/CD để tự động hóa kiểm thử và đảm bảo chất lượng liên tục.