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)
unittest 核心组件协作流程 TestCase test_get_user() test_create_user() test_delete_user() TestSuite addTest(TestCase) addTests(TestLoader) TestRunner run(TestSuite) 输出测试报告 💡 组件协作说明 1. TestCase 定义具体的测试逻辑(断言 + 夹具) 2. TestSuite 将多个 TestCase 按模块/功能组织成测试集 3. TestRunner 执行 TestSuite 并生成结构化测试报告

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
测试夹具执行生命周期 setUpClass() setUp() test_*() tearDown() tearDownClass() 📋 执行顺序说明 1. setUpClass() — 类加载时执行一次 用于初始化全局资源(如配置、连接池) 2. setUp() — 每个测试方法前执行 用于创建 Mock、准备测试数据 3. test_*() — 测试方法执行 包含断言逻辑,验证 API 行为 4. tearDown() — 每个测试方法后执行 用于清理 Mock、删除测试数据 5. tearDownClass() — 类卸载时执行一次 用于释放全局资源

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 运行测试可以获得更详细的执行日志,快速定位失败点。