Skip to content

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_codejson()textheaders 等属性。

二、pytest 集成

bash
uv add pytest
python
# 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 data

json= 参数自动设置 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