第 11 章:API 测试与文档 — 示例与习题
本章摘要:本章围绕 Flask API 的测试与文档展开,通过三个示例演示了完整的测试方法:使用 VS Code REST Client 编写 HTTP 请求测试文件、使用 pytest 编写单元测试(覆盖用户 CRUD、分页、搜索等场景)、以及使用 requests 库编写集成测试脚本。随后提供 5 道习题(选择题、填空题、判断题、实践题)帮助巩固知识,并附有详细答案与解析,其中实践题要求为产品管理 API 编写完整的 pytest 测试套件。
示例
示例 1:VS Code REST Client 测试文件
### 创建用户
POST http://127.0.0.1:5000/api/v1/users
Content-Type: application/json
{
"username": "testuser",
"email": "test@example.com"
}
### 获取用户列表
GET http://127.0.0.1:5000/api/v1/users?page=1&per_page=10
### 搜索用户
GET http://127.0.0.1:5000/api/v1/users?search=test
### 获取单个用户
GET http://127.0.0.1:5000/api/v1/users/1
### 更新用户(PATCH 部分更新)
PATCH http://127.0.0.1:5000/api/v1/users/1
Content-Type: application/json
{
"email": "newemail@example.com"
}
### 删除用户
DELETE http://127.0.0.1:5000/api/v1/users/1
### 创建文章
POST http://127.0.0.1:5000/api/v1/posts
Content-Type: application/json
{
"title": "Flask API 测试",
"content": "今天学习如何测试 API…",
"user_id": 1
}
### 获取用户 1 的文章
GET http://127.0.0.1:5000/api/v1/users/1/posts
### 错误测试:缺少必填字段
POST http://127.0.0.1:5000/api/v1/users
Content-Type: application/json
{
"email": "missing-username@example.com"
}
### 错误测试:获取不存在的资源
GET http://127.0.0.1:5000/api/v1/users/999
示例 2:pytest 单元测试
import pytest
from app import create_app, db
from app.models import User
@pytest.fixture
def app():
app = create_app('testing')
with app.app_context():
db.create_all()
# 添加测试数据
users = [
User(username='user1', email='user1@test.com'),
User(username='user2', email='user2@test.com'),
User(username='admin', email='admin@test.com', role='admin'),
]
db.session.add_all(users)
db.session.commit()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
class TestUserList:
def test_get_all_users(self, client):
resp = client.get('/api/v1/users')
assert resp.status_code == 200
data = resp.get_json()
assert data['code'] == 200
assert len(data['data']) == 3
def test_pagination(self, client):
resp = client.get('/api/v1/users?per_page=2')
assert resp.status_code == 200
data = resp.get_json()
assert len(data['data']) == 2
assert data['pagination']['total'] == 3
assert data['pagination']['pages'] == 2
def test_search(self, client):
resp = client.get('/api/v1/users?search=admin')
data = resp.get_json()
assert len(data['data']) == 1
assert data['data'][0]['username'] == 'admin'
class TestCreateUser:
def test_create_success(self, client):
resp = client.post('/api/v1/users', json={
'username': 'newuser', 'email': 'new@test.com'
})
assert resp.status_code == 201
assert resp.get_json()['data']['username'] == 'newuser'
def test_create_missing_field(self, client):
resp = client.post('/api/v1/users', json={'email': 'no-username@test.com'})
assert resp.status_code == 400
def test_create_duplicate(self, client):
resp = client.post('/api/v1/users', json={
'username': 'user1', 'email': 'diff@test.com'
})
assert resp.status_code == 409
class TestGetUser:
def test_get_existing(self, client):
resp = client.get('/api/v1/users/1')
assert resp.status_code == 200
assert resp.get_json()['data']['id'] == 1
def test_get_not_found(self, client):
resp = client.get('/api/v1/users/999')
assert resp.status_code == 404
class TestDeleteUser:
def test_delete_success(self, client):
resp = client.delete('/api/v1/users/2')
assert resp.status_code == 200
# 验证确实删除了
resp2 = client.get('/api/v1/users/2')
assert resp2.status_code == 404
def test_delete_not_found(self, client):
resp = client.delete('/api/v1/users/999')
assert resp.status_code == 404
class TestUpdateUser:
def test_patch_partial_update(self, client):
resp = client.patch('/api/v1/users/1', json={'email': 'updated@test.com'})
assert resp.status_code == 200
assert resp.get_json()['data']['email'] == 'updated@test.com'
# username 不应改变
assert resp.get_json()['data']['username'] == 'user1'
def test_put_full_update(self, client):
resp = client.put('/api/v1/users/1', json={
'username': 'user1', 'email': 'new@test.com', 'role': 'editor'
})
assert resp.status_code == 200
data = resp.get_json()['data']
assert data['email'] == 'new@test.com'
assert data['role'] == 'editor'
示例 3:用 requests 库编写集成测试
import requests
import sys
BASE_URL = "http://127.0.0.1:5000/api/v1"
passed = 0
failed = 0
def test(name, condition, detail=""):
global passed, failed
if condition:
print(f" ✓ {name}")
passed += 1
else:
print(f" ✗ {name} — {detail}")
failed += 1
# === 测试套件 ===
print("=== 用户 API 测试 ===")
# 创建用户
resp = requests.post(f"{BASE_URL}/users", json={
"username": "testuser", "email": "test@test.com"
})
test("创建用户 – 状态码 201", resp.status_code == 201, f"实际: {resp.status_code}")
test("创建用户 – 返回数据正确", resp.json().get('data', {}).get('username') == 'testuser")
user_id = resp.json().get('data', {}).get('id')
# 获取用户列表
resp = requests.get(f"{BASE_URL}/users")
test("获取列表 – 状态码 200", resp.status_code == 200)
test("获取列表 – 包含数据", len(resp.json().get('data', [])) > 0)
# 获取单个用户
resp = requests.get(f"{BASE_URL}/users/{user_id}")
test("获取单个 – 状态码 200", resp.status_code == 200)
test("获取单个 – ID 正确", resp.json()['data']['id'] == user_id)
# 更新用户
resp = requests.patch(f"{BASE_URL}/users/{user_id}", json={"email": "updated@test.com"})
test("更新用户 – 状态码 200", resp.status_code == 200)
test("更新用户 – 邮箱已更新", resp.json()['data']['email'] == 'updated@test.com')
# 错误场景
resp = requests.get(f"{BASE_URL}/users/999")
test("获取不存在 – 状态码 404", resp.status_code == 404)
resp = requests.post(f"{BASE_URL}/users", json={"email": "no-username@test.com"})
test("缺少字段 – 状态码 400", resp.status_code == 400)
resp = requests.post(f"{BASE_URL}/users", json={"username": "testuser", "email": "dup@test.com"})
test("重复用户名 – 状态码 409", resp.status_code == 409)
# 删除用户
resp = requests.delete(f"{BASE_URL}/users/{user_id}")
test("删除用户 – 状态码 200", resp.status_code == 200)
resp = requests.get(f"{BASE_URL}/users/{user_id}")
test("删除后获取 – 状态码 404", resp.status_code == 404)
# 汇总
print(f"\\n{'=' * 40}")
print(f"通过: {passed} 失败: {failed} 总计: {passed + failed}")
sys.exit(0 if failed == 0 else 1)
习题
习题 1(选择题)
pytest 中,@pytest.fixture 的作用是?
A. 标记测试为跳过
B. 提供测试前的准备数据和环境(如测试客户端)
C. 参数化测试
D. 标记预期失败
习题 2(选择题)
在 pytest 测试 Flask API 时,获取 POST 请求的 JSON 响应应使用?
A. response.text
B. response.json()
C. response.get_json()
D. B 和 C 都可以
习题 3(填空题)
在 VS Code REST Client 中,每个 HTTP 请求之间用 ________ 分隔,点击请求上方的 ________ 链接发送请求。
习题 4(判断题)
在 pytest 中,每个测试函数都共享同一个测试数据库实例,修改数据会影响其他测试。( )
习题 5(实践题)
为第 10 章的产品管理 API 编写完整的 pytest 测试套件:
- 获取产品列表(含分页)
- 搜索产品
- 按价格范围筛选
- 获取单个产品(存在/不存在)
- 创建产品(成功/缺少字段/价格<=0)
- 更新产品
- 删除产品(成功/不存在)
答案
习题 1 答案
B. 提供测试前的准备数据和环境
解析:fixture 是 pytest 的依赖注入机制,在测试前创建和清理测试环境。如创建测试应用、测试客户端、数据库等。yield 之后的代码在测试结束后执行(清理)。
习题 2 答案
D. B 和 C 都可以
解析:response.get_json() 是 Flask 测试客户端的方法;如果用 requests 库则是 response.json()。两者都能将 JSON 响应体解析为 Python 字典。
习题 3 答案
###(三个井号);Send Request
解析:.http 文件中用 ### 分隔多个请求。每个请求上方会出现 “Send Request” 链接,点击即可发送该请求。
习题 4 答案
错误(×)
解析:正确的做法是每个测试使用独立的 fixture,测试间不应共享状态。使用 yield fixture 在每个测试后清理数据,或使用内存数据库(sqlite://)让每个测试自动获得干净的数据库。
习题 5 答案
import pytest
from app import create_app, db
from app.models import Product
@pytest.fixture
def app():
app = create_app('testing')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
with app.app_context():
db.create_all()
products = [
Product(name='键盘', price=199, stock=50, description='机械键盘'),
Product(name='鼠标', price=89, stock=100, description='无线鼠标'),
Product(name='显示器', price=1299, stock=10, description='27寸4K'),
]
db.session.add_all(products)
db.session.commit()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
class TestListProducts:
def test_get_all(self, client):
resp = client.get('/api/v1/products')
assert resp.status_code == 200
data = resp.get_json()
assert data['code'] == 200
assert len(data['data']) == 3
def test_pagination(self, client):
resp = client.get('/api/v1/products?per_page=2')
data = resp.get_json()
assert len(data['data']) == 2
assert data['pagination']['total'] == 3
def test_search(self, client):
resp = client.get('/api/v1/products?search=键')
data = resp.get_json()
assert len(data['data']) == 1
assert data['data'][0]['name'] == '键盘'
def test_price_range(self, client):
resp = client.get('/api/v1/products?min_price=100&max_price=200')
data = resp.get_json()
assert len(data['data']) == 1
assert data['data'][0]['name'] == '键盘'
class TestGetProduct:
def test_existing(self, client):
resp = client.get('/api/v1/products/1')
assert resp.status_code == 200
assert resp.get_json()['data']['id'] == 1
def test_not_found(self, client):
resp = client.get('/api/v1/products/999')
assert resp.status_code == 404
class TestCreateProduct:
def test_success(self, client):
resp = client.post('/api/v1/products', json={
'name': '耳机', 'price': 299, 'stock': 30
})
assert resp.status_code == 201
assert resp.get_json()['data']['name'] == '耳机'
def test_missing_name(self, client):
resp = client.post('/api/v1/products', json={'price': 100})
assert resp.status_code == 400
def test_invalid_price(self, client):
resp = client.post('/api/v1/products', json={'name': '测试', 'price': –10})
assert resp.status_code == 400
class TestUpdateProduct:
def test_update_price(self, client):
resp = client.put('/api/v1/products/1', json={'price': 250})
assert resp.status_code == 200
assert resp.get_json()['data']['price'] == 250
def test_not_found(self, client):
resp = client.put('/api/v1/products/999', json={'price': 100})
assert resp.status_code == 404
class TestDeleteProduct:
def test_success(self, client):
resp = client.delete('/api/v1/products/2')
assert resp.status_code == 200
resp2 = client.get('/api/v1/products/2')
assert resp2.status_code == 404
def test_not_found(self, client):
resp = client.delete('/api/v1/products/999')
assert resp.status_code == 404



