3. Python unittest 框架基础:核心组件与测试夹具
在 API 设计治理中,unittest 是 Python 标准库内置的测试框架,为 Mock 隔离与故障排查提供了最基础的执行环境。掌握 unittest 的核心组件、断言方法和测试夹具,是构建高质量 API 测试套件的第一步。
3.1 核心组件:TestCase、TestSuite、TestRunner
unittest 框架围绕三个核心组件构建,它们分别承担测试定义、测试组织和测试执行的角色:
| 组件 | 职责 | 典型用法 |
|---|---|---|
| TestCase | 定义单个测试用例,包含测试方法和夹具 | 继承 unittest.TestCase,编写以 test_ 开头的方法 |
| TestSuite | 组织和聚合多个 TestCase 或 TestSuite | 使用 TestSuite.addTest() 或 TestLoader 批量加载 |
| TestRunner | 驱动测试执行,输出结果 | 使用 TextTestRunner 运行测试并生成报告 |
API 治理提示: 在 API 测试中,每个 TestCase 通常对应一个 API 端点或一个业务场景。将测试按资源(如用户、订单)组织到不同的 TestCase 类中,再通过 TestSuite 按模块聚合,便于后续集成 Mock 和故障排查。
以下代码展示了三个组件的协作关系:
import unittest
# 1. 定义 TestCase
class TestUserAPI(unittest.TestCase):
def test_get_user(self):
# 模拟调用 GET /users/1
self.assertEqual(200, 200) # 占位断言
def test_create_user(self):
self.assertEqual(201, 201)
# 2. 组织 TestSuite
suite = unittest.TestSuite()
suite.addTest(TestUserAPI('test_get_user'))
suite.addTest(TestUserAPI('test_create_user'))
# 3. 执行 TestRunner
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
3.2 断言方法:验证 API 响应的核心工具
断言是测试的“裁判”,用于判断实际结果是否符合预期。unittest 提供了丰富的断言方法,覆盖 API 测试中的常见验证场景:
| 断言方法 | 用途 | API 测试场景示例 |
|---|---|---|
assertEqual(a, b) |
检查 a == b | 验证 HTTP 状态码是否为 200 |
assertNotEqual(a, b) |
检查 a != b | 验证错误响应不是 200 |
assertTrue(x) |
检查 x 为 True | 验证响应中包含特定字段 |
assertFalse(x) |
检查 x 为 False | 验证删除操作后资源不存在 |
assertIn(a, b) |
检查 a in b | 验证错误消息包含特定关键词 |
assertIsNone(x) |
检查 x 为 None | 验证可选字段在无值时返回 null |
assertRaises(Exc, fun) |
检查调用 fun 时抛出 Exc | 验证无效参数触发 ValueError |
最佳实践: 在 API 测试中,优先使用语义明确的断言方法(如
assertEqual 而非 assertTrue(a == b)),这样在测试失败时能获得更清晰的错误信息,加速故障排查。
以下代码展示了在 API 测试中使用断言的实际场景:
import unittest
import json
class TestOrderAPI(unittest.TestCase):
def test_create_order_success(self):
# 模拟 API 响应
response_status = 201
response_body = {"order_id": "12345", "status": "created"}
# 断言状态码
self.assertEqual(response_status, 201, "创建订单应返回 201")
# 断言响应体包含必要字段
self.assertIn("order_id", response_body, "响应应包含 order_id")
self.assertEqual(response_body["status"], "created", "订单状态应为 created")
def test_create_order_invalid_payload(self):
# 模拟无效请求的响应
response_status = 400
response_body = {"error": "missing field: user_id"}
self.assertEqual(response_status, 400)
self.assertIn("missing field", response_body["error"])
3.3 测试夹具:setUp / tearDown 与资源管理
测试夹具(Test Fixture)用于在测试前后执行准备和清理工作,确保每个测试用例在干净的环境中运行。unittest 通过 setUp() 和 tearDown() 方法实现夹具管理。
| 夹具方法 | 执行时机 | API 测试典型用途 |
|---|---|---|
setUp() |
每个测试方法执行前 | 初始化 Mock 对象、创建测试数据、建立连接 |
tearDown() |
每个测试方法执行后 | 清理测试数据、关闭连接、重置 Mock 状态 |
setUpClass() |
整个测试类执行前(仅一次) | 加载配置文件、初始化数据库连接池 |
tearDownClass() |
整个测试类执行后(仅一次) | 释放全局资源、关闭连接池 |
常见陷阱: 在
setUp() 中创建的 Mock 对象,如果不在 tearDown() 中清理,可能会在测试间产生状态泄漏。建议在 tearDown() 中调用 mock.reset_mock() 或 mock.stop() 确保隔离。
以下代码展示了夹具在 API 测试中的完整应用:
import unittest
from unittest.mock import patch
import requests
class TestPaymentAPI(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 类级别:加载测试配置(仅执行一次)
cls.base_url = "https://api.example.com/v1"
cls.timeout = 5
def setUp(self):
# 方法级别:每个测试前准备 Mock
self.patcher = patch('requests.post')
self.mock_post = self.patcher.start()
# 设置默认 Mock 返回值
self.mock_post.return_value.status_code = 200
self.mock_post.return_value.json.return_value = {"status": "success"}
def tearDown(self):
# 方法级别:每个测试后清理 Mock
self.patcher.stop()
# 清理测试数据(示例)
self._clean_test_data()
def test_payment_success(self):
response = requests.post(f"{self.base_url}/payments", json={"amount": 100})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], "success")
def test_payment_failure(self):
# 覆盖 Mock 返回值模拟失败场景
self.mock_post.return_value.status_code = 402
self.mock_post.return_value.json.return_value = {"error": "insufficient funds"}
response = requests.post(f"{self.base_url}/payments", json={"amount": 9999})
self.assertEqual(response.status_code, 402)
self.assertIn("insufficient", response.json()["error"])
def _clean_test_data(self):
# 私有方法:清理测试产生的临时数据
pass
@classmethod
def tearDownClass(cls):
# 类级别:释放全局资源(仅执行一次)
cls.base_url = None
3.4 综合示例:API 测试夹具与断言实战
将核心组件、断言和夹具整合到一个完整的 API 测试场景中,展示如何对用户管理接口进行测试:
import unittest
from unittest.mock import patch, MagicMock
import json
class TestUserManagementAPI(unittest.TestCase):
"""用户管理 API 测试套件"""
@classmethod
def setUpClass(cls):
cls.base_url = "https://api.example.com/users"
cls.headers = {"Authorization": "Bearer test-token"}
def setUp(self):
# 创建 Mock 对象,隔离外部 HTTP 调用
self.mock_get = patch('requests.get').start()
self.mock_post = patch('requests.post').start()
self.mock_delete = patch('requests.delete').start()
def tearDown(self):
patch.stopall() # 停止所有 Mock
self._clean_test_users()
def _clean_test_users(self):
"""清理测试中创建的用户数据"""
# 实际项目中可调用清理接口或数据库操作
pass
def test_get_user_by_id_success(self):
"""测试根据 ID 获取用户成功"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": 1, "name": "Alice", "email": "alice@example.com"
}
self.mock_get.return_value = mock_response
response = self.mock_get(f"{self.base_url}/1", headers=self.headers)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["name"], "Alice")
self.assertIn("email", response.json())
def test_create_user_duplicate_email(self):
"""测试创建重复邮箱用户应返回 409"""
mock_response = MagicMock()
mock_response.status_code = 409
mock_response.json.return_value = {"error": "email already exists"}
self.mock_post.return_value = mock_response
payload = {"name": "Bob", "email": "bob@example.com"}
response = self.mock_post(self.base_url, json=payload, headers=self.headers)
self.assertEqual(response.status_code, 409)
self.assertIn("already exists", response.json()["error"])
def test_delete_user_unauthorized(self):
"""测试未授权删除应返回 401"""
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.json.return_value = {"error": "unauthorized"}
self.mock_delete.return_value = mock_response
response = self.mock_delete(f"{self.base_url}/1")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["error"], "unauthorized")
if __name__ == '__main__':
unittest.main(verbosity=2)
故障排查要点: 当测试失败时,首先检查
setUp() 中的 Mock 配置是否正确,其次确认断言方法是否匹配实际响应结构。使用 verbosity=2 运行测试可以获得更详细的执行日志,快速定位失败点。