欢迎光临
我们一直在努力

Python web编程教程(二十二)

第 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 测试套件:

  • 创建测试夹具(fixture),使用内存数据库
  • 预置 3 条产品数据
  • 测试以下场景:
    • 获取产品列表(含分页)
    • 搜索产品
    • 按价格范围筛选
    • 获取单个产品(存在/不存在)
    • 创建产品(成功/缺少字段/价格<=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

    赞(0)
    未经允许不得转载:171主机测评 » Python web编程教程(二十二)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址