Appearance
49|接口测试与 TestClient
代码写得再严谨,也需要测试验证。FastAPI 提供 TestClient,可以在不启动 HTTP 服务器的情况下测试接口——它直接把请求发给 FastAPI 应用,然后返回响应。测试和代码在同一个进程中运行,调试方便、执行快速。
一、TestClient 基础
python
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() == {"message": "hello"}TestClient 的使用方式和 requests 库几乎一样:.get()、.post()、.put()、.delete(),返回的响应对象有 status_code、json()、text、headers 等属性。
二、pytest 集成
bash
uv add pytestpython
# tests/test_servers.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_list_servers():
response = client.get("/servers")
assert response.status_code == 200
data = response.json()
assert "items" in data
assert isinstance(data["items"], list)
def test_get_server_not_found():
response = client.get("/servers/99999")
assert response.status_code == 404
assert response.json()["code"] == 404运行测试:
bash
uv run pytest
uv run pytest -v # 详细输出
uv run pytest -k "server" # 只跑名字含 server 的测试三、测试 POST 请求
python
def test_create_server():
response = client.post(
"/servers",
json={"hostname": "test-web", "ip": "192.168.1.100"},
)
assert response.status_code == 201
data = response.json()
assert data["hostname"] == "test-web"
assert "id" in datajson= 参数自动设置 Content-Type: application/json 并把字典序列化为 JSON。
表单数据
python
def test_login():
response = client.post(
"/login",
data={"username": "admin", "password": "secret"},
)
assert response.status_code == 200
assert "access_token" in response.json()data= 发送 application/x-www-form-urlencoded 格式。
上传文件
python
def test_upload():
response = client.post(
"/upload",
files={"file": ("test.csv", b"id,name\n1,a\n", "text/csv")},
)
assert response.status_code == 200四、测试认证
python
def test_protected_endpoint_without_auth():
response = client.get("/api/me")
assert response.status_code == 401
def test_protected_endpoint_with_auth():
# 先登录获取 token
login_resp = client.post("/login", data={"username": "admin", "password": "pass"})
token = login_resp.json()["access_token"]
# 带 Token 访问
response = client.get(
"/api/me",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
assert response.json()["name"] == "admin"五、fixture 复用
python
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def auth_client(client):
login_resp = client.post("/login", data={"username": "admin", "password": "pass"})
token = login_resp.json()["access_token"]
client.headers["Authorization"] = f"Bearer {token}"
return client
def test_list_servers(client):
response = client.get("/servers")
assert response.status_code == 200
def test_create_server(auth_client):
response = auth_client.post("/servers", json={"hostname": "web-01"})
assert response.status_code == 201| fixture | 用途 |
|---|---|
client | 未认证的 TestClient |
auth_client | 已登录、带 Token 的 TestClient |
六、数据库隔离
测试不应污染开发数据库。用 SQLite 内存数据库:
python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base, get_db
from main import app
# 内存数据库,每个测试独立
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture
def db():
Base.metadata.create_all(bind=engine)
session = TestingSessionLocal()
yield session
session.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(db):
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
del app.dependency_overrides[get_db]dependency_overrides 临时替换 FastAPI 的依赖,让测试路由使用内存数据库的会话。
七、常见错误
测试中使用外部服务
python
# 错误:测试依赖外部 SMTP 服务,不稳定
def test_email():
send_email() # 真的发邮件
# 正确:Mock 外部服务
from unittest.mock import patch
def test_email():
with patch("module.send_email") as mock:
mock.return_value = True
result = send_email()
assert result is True测试依赖执行顺序
python
# 错误:test_b 依赖 test_a 创建的数据
# pytest 不保证测试按文件顺序执行
# 正确:每个测试独立准备数据
@pytest.fixture
def sample_server(db):
server = Server(hostname="test", ip="1.1.1.1")
db.add(server)
db.commit()
return server断言 JSON 时用 == 比较浮点数
python
# 错误:浮点数精度问题
assert response.json()["usage"] == 33.3333333333
# 正确:近似比较
assert abs(response.json()["usage"] - 33.33) < 0.01