第 11 章:API 测试与文档
本章摘要:本章围绕 Flask 博客系统的 API 测试与文档展开。首先介绍使用 Postman 进行可视化接口测试,包括基本请求、测试集与环境变量的使用;随后讲解 VS Code REST Client 扩展的轻量替代方案。接着重点演示如何用 pytest 编写自动化测试,覆盖用户列表、创建、查询与删除等核心接口,并给出完整测试代码与运行输出。之后引入 Flasgger(Swagger)为 API 自动生成交互式文档,展示集成步骤与路由文档写法。最后总结自动化测试的最佳实践,强调测试先行(TDD)对保障代码质量的重要性。
11.1 用 Postman 测试 API
Postman 是最流行的 API 测试工具。你可以可视化地发送 HTTP 请求、查看响应、管理测试集。
基本使用:
- 选择 “Body” 标签
- 选择 “raw” → “JSON”
- 输入 JSON 数据
创建测试集(Collection):
把相关的请求组织在一起,方便反复运行:
环境变量:
Environment: Development
base_url = http://127.0.0.1:5000
api_version = api/v1
请求 URL 写成 {{base_url}}/{{api_version}}/users,切换环境时只需改变量值。
11.2 使用 VS Code REST Client
如果不想安装 Postman,VS Code 的 REST Client 扩展同样好用。创建 .http 文件直接发请求:
### 获取用户列表
GET http://127.0.0.1:5000/api/v1/users?page=1&per_page=5
### 创建用户
POST http://127.0.0.1:5000/api/v1/users
Content-Type: application/json
{
"username": "newuser",
"email": "new@example.com",
"role": "user"
}
### 获取单个用户
GET http://127.0.0.1:5000/api/v1/users/1
### 更新用户
PATCH http://127.0.0.1:5000/api/v1/users/1
Content-Type: application/json
{
"role": "editor"
}
### 删除用户
DELETE http://127.0.0.1:5000/api/v1/users/2
### 创建文章
POST http://127.0.0.1:5000/api/v1/posts
Content-Type: application/json
{
"title": "我的第一篇文章",
"content": "这是内容…",
"user_id": 1
}
每个 ### 分隔一个请求,点击代码上方的 “Send Request” 即可发送。
11.3 用 Python 写自动化测试
手动测试效率低且容易遗漏。自动化测试能保证代码修改后功能不退化。
安装 pytest:
pip install pytest
编写测试:
# tests/test_user_api.py
import pytest
import json
from app import create_app, db
from app.models import User
@pytest.fixture
def app():
"""每个测试使用独立的内存数据库"""
app = create_app('testing')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' # 内存数据库
with app.app_context():
db.create_all()
# 添加测试数据
user1 = User(username='user1', email='user1@test.com')
user2 = User(username='user2', email='user2@test.com')
db.session.add_all([user1, user2])
db.session.commit()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
"""测试客户端"""
return app.test_client()
class TestGetUsers:
"""测试获取用户列表"""
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']) == 2
def test_pagination(self, client):
resp = client.get('/api/v1/users?per_page=1')
data = resp.get_json()
assert len(data['data']) == 1
assert data['pagination']['total'] == 2
assert data['pagination']['pages'] == 2
def test_search(self, client):
resp = client.get('/api/v1/users?search=user1')
data = resp.get_json()
assert len(data['data']) == 1
assert data['data'][0]['username'] == 'user1'
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
data = resp.get_json()
assert data['data']['username'] == 'newuser'
def test_create_missing_username(self, client):
resp = client.post('/api/v1/users', json={
'email': 'test@test.com'
})
assert resp.status_code == 400
def test_create_duplicate(self, client):
resp = client.post('/api/v1/users', json={
'username': 'user1',
'email': 'different@test.com'
})
assert resp.status_code == 409
class TestGetSingleUser:
"""测试获取单个用户"""
def test_get_existing(self, client):
resp = client.get('/api/v1/users/1')
assert resp.status_code == 200
data = resp.get_json()
assert data['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/1')
assert resp.status_code == 200
def test_delete_not_found(self, client):
resp = client.delete('/api/v1/users/999')
assert resp.status_code == 404
运行测试:
pytest tests/ -v
输出:
tests/test_user_api.py::TestGetUsers::test_get_all_users PASSED
tests/test_user_api.py::TestGetUsers::test_pagination PASSED
tests/test_user_api.py::TestGetUsers::test_search PASSED
tests/test_user_api.py::TestCreateUser::test_create_success PASSED
tests/test_user_api.py::TestCreateUser::test_create_missing_username PASSED
tests/test_user_api.py::TestCreateUser::test_create_duplicate PASSED
tests/test_user_api.py::TestGetSingleUser::test_get_existing PASSED
tests/test_user_api.py::TestGetSingleUser::test_get_not_found PASSED
tests/test_user_api.py::TestDeleteUser::test_delete_success PASSED
tests/test_user_api.py::TestDeleteUser::test_delete_not_found PASSED
10 passed in 0.45s
11.4 API 文档:Flasgger (Swagger)
API 文档让使用者知道有哪些接口、怎么调用。Swagger/OpenAPI 是 API 文档的标准。
安装 Flasgger:
pip install flasgger
集成到 Flask:
from flasgger import Swagger
def create_app(config_name='default'):
app = Flask(__name__)
# … 其他配置 …
swagger_config = {
'headers': [],
'specs': [{
'endpoint': 'apispec',
'route': '/apispec.json',
'rule_filter': lambda rule: True,
'model_filter': lambda tag: True,
}],
'static_url_path': '/flasgger_static',
'swagger_ui': True,
'specs_route': '/docs/'
}
swagger_template = {
'info': {
'title': 'Flask Blog API',
'description': '一个博客系统的 RESTful API',
'version': '1.0.0',
}
}
Swagger(app, config=swagger_config, template=swagger_template)
# … 其他代码 …
return app
在路由中添加文档:
@user_bp.route('/users', methods=['GET'])
def get_users():
"""
获取用户列表
—
tags:
– 用户
parameters:
– name: page
in: query
type: integer
default: 1
description: 页码
– name: per_page
in: query
type: integer
default: 10
description: 每页条数
– name: search
in: query
type: string
description: 搜索关键词
responses:
200:
description: 成功获取用户列表
schema:
type: object
properties:
code:
type: integer
example: 200
data:
type: array
items:
type: object
properties:
id:
type: integer
username:
type: string
"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
search = request.args.get('search', '').strip()
query = User.query
if search:
query = query.filter(
User.username.contains(search) | User.email.contains(search)
)
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
return api_response(
data=[user.to_dict() for user in pagination.items],
pagination={
'page': pagination.page,
'per_page': pagination.per_page,
'total': pagination.total,
'pages': pagination.pages,
}
)
运行后访问 http://127.0.0.1:5000/docs/,你会看到自动生成的 API 文档页面,包含:
- 所有接口列表
- 每个接口的参数说明
- 在线测试功能(直接在文档页面发请求)
11.5 自动化测试的最佳实践
# tests/conftest.py —— 公共测试夹具
import pytest
from app import create_app, db
@pytest.fixture
def app():
"""创建测试应用"""
app = create_app('testing')
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' # 内存数据库,测试完自动消失
with app.app_context():
db.create_all()
# 初始化测试数据
from app.models import User, Post
user = User(username='admin', email='admin@test.com', role='admin')
db.session.add(user)
db.session.commit()
post = Post(title='测试文章', content='测试内容', user_id=1)
db.session.add(post)
db.session.commit()
yield app
with app.app_context():
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app):
"""测试客户端"""
return app.test_client()
@pytest.fixture
def runner(app):
"""CLI 测试运行器"""
return app.test_cli_runner()
# 运行所有测试
pytest
# 运行特定文件
pytest tests/test_user_api.py
# 运行特定测试类
pytest tests/test_user_api.py::TestCreateUser
# 运行特定测试方法
pytest tests/test_user_api.py::TestCreateUser::test_create_success
# 显示详细输出
pytest -v
# 显示打印输出
pytest -s
# 生成覆盖率报告
pip install pytest-cov
pytest –cov=app tests/
💡 提示
测试不是可选的“额外工作”——它是专业开发的标配。好的测试能让你放心地重构代码、添加功能,而不担心“改了这里会不会弄坏那里”。测试先行(TDD)是一种很好的习惯:先写测试描述预期行为,再写代码让它通过。
网硕互联帮助中心


评论前必须登录!
注册