Chapter 07

AI 辅助测试生成

从边界条件发现到 Mock 生成,让 AI 成为你最高效的测试工程师搭档

AI 生成测试的优势与局限

AI 真正擅长的测试任务

测试编写是 AI 工具效果最好的应用场景之一。AI 特别擅长:

AI 测试的局限性

警惕"过拟合实现"的测试

AI 生成的测试最大的风险是:测试可能只是对当前实现的描述,而不是对期望行为的规范。例如,如果实现中有 Bug,AI 可能生成一个"验证 Bug 行为"的测试,而不是一个"描述正确行为"的测试。务必审查每个测试用例的意图。

从函数签名生成单元测试

最高效的触发方式:先写类型,后生成测试

# 步骤1:写好带类型注释的函数签名和文档字符串
def calculate_discount(
    original_price: float,
    user_tier: str,  # "standard" | "premium" | "vip"
    coupon_code: str | None = None,
    quantity: int = 1
) -> tuple[float, float]:
    """
    计算折后价格和节省金额。

    折扣规则:
    - standard 用户:无折扣
    - premium 用户:九折
    - vip 用户:八折
    - 优惠码 SAVE20:额外九折(与用户折扣叠加,不低于七折)
    - quantity >= 10:额外九五折(在所有折扣后叠加)

    Returns:
        (final_price, saved_amount)

    Raises:
        ValueError: 当 original_price <= 0 或 quantity <= 0 时
        ValueError: 当 user_tier 不在有效范围内时
    """
    ...
# AI 生成的测试(给 AI 的提示)
"""
请为上面的 calculate_discount 函数生成全面的 pytest 单元测试。
要求:
1. 测试所有折扣规则的组合
2. 测试边界条件(price 为 0、负数;quantity 为 0、1、9、10)
3. 测试无效的 user_tier
4. 使用 parametrize 减少重复代码
5. 每个测试有清晰的描述
"""

# AI 输出的测试代码
import pytest
from decimal import Decimal


class TestCalculateDiscount:

    @pytest.mark.parametrize("user_tier,expected_discount", [
        ("standard", 1.0),    # 无折扣
        ("premium", 0.9),    # 九折
        ("vip", 0.8),        # 八折
    ])
    def test_user_tier_discounts(self, user_tier, expected_discount):
        """各用户等级折扣应正确应用"""
        price = 100.0
        final, saved = calculate_discount(price, user_tier)
        assert abs(final - price * expected_discount) < 0.001
        assert abs(saved - price * (1 - expected_discount)) < 0.001

    def test_coupon_stacks_with_premium(self):
        """优惠码与 premium 折扣叠加"""
        final, _ = calculate_discount(100.0, "premium", coupon_code="SAVE20")
        # 九折 × 九折 = 八一折 = 81.0
        assert abs(final - 81.0) < 0.001

    def test_floor_discount_at_70_percent(self):
        """折扣叠加不低于七折(vip + SAVE20)"""
        final, _ = calculate_discount(100.0, "vip", coupon_code="SAVE20")
        # 八折 × 九折 = 七二折,高于七折,不触发底线
        assert abs(final - 72.0) < 0.001

    @pytest.mark.parametrize("quantity", [10, 100, 1000])
    def test_bulk_discount_applies_at_10(self, quantity):
        """quantity >= 10 时应用九五折"""
        final, _ = calculate_discount(100.0, "standard", quantity=quantity)
        assert abs(final - 95.0) < 0.001

    @pytest.mark.parametrize("invalid_price", [0, -1, -100])
    def test_invalid_price_raises_value_error(self, invalid_price):
        """无效价格应抛出 ValueError"""
        with pytest.raises(ValueError):
            calculate_discount(invalid_price, "standard")

    def test_invalid_user_tier_raises_value_error(self):
        """无效的用户等级应抛出 ValueError"""
        with pytest.raises(ValueError, match="user_tier"):
            calculate_discount(100.0, "gold")  # "gold" 不是有效等级

边界条件系统化发现

边界值分析(BVA)Prompt 模板

请对以下函数进行系统化的边界条件分析:

```{language}
{function_code}
```

请列出以下类型的边界条件:

1. **数值边界**
   - 零值、负数、极大/极小值
   - 浮点精度问题(0.1 + 0.2 != 0.3)
   - 整数溢出风险

2. **字符串边界**
   - 空字符串 ""
   - 只有空格的字符串
   - 极长字符串(>1000 字符)
   - 包含特殊字符(\n, \t, \0, Unicode)
   - SQL/HTML 注入字符串

3. **集合/列表边界**
   - 空列表/集合
   - 只有一个元素
   - 重复元素
   - None vs 空集合的区别

4. **None/null 边界**
   - 直接传入 None
   - Optional 参数未传入(使用默认值)

5. **并发边界**(如果适用)
   - 同时调用会产生什么问题?

对每个边界条件,说明:
- 当前代码是否正确处理
- 如果没有,会发生什么(崩溃/错误结果/无声失败)

生成 Mock 和 Stub

Mock 的场景和策略

Mock(模拟对象)
替代整个对象,可以验证方法是否被调用、调用了几次、传入了什么参数。适合验证"行为"(如:是否发送了邮件)。
Stub(存根)
只替代方法的返回值,不关心是否被调用。适合隔离外部依赖(如:数据库返回固定数据)。
Fake(假对象)
有真实实现但简化版的对象(如:内存数据库替代真实数据库)。适合集成测试的中间层。
Spy(间谍)
包装真实对象,记录调用但仍执行真实逻辑。适合不想完全替换某个对象但需要观察调用的情况。
# AI 生成 Mock 的提示示例
"""
为以下服务类生成完整的测试代码,
Mock 掉所有外部依赖(数据库、邮件服务、S3)。
使用 pytest-mock 的 mocker fixture。
"""

class UserService:
    def __init__(self, db: Database, email_service: EmailService, s3: S3Client):
        self.db = db
        self.email_service = email_service
        self.s3 = s3

    async def create_user(self, user_data: UserCreateSchema) -> UserModel:
        # 检查邮箱唯一性
        if await self.db.users.exists(email=user_data.email):
            raise DuplicateEmailError(user_data.email)
        # 创建用户
        user = await self.db.users.create(user_data)
        # 发送欢迎邮件
        await self.email_service.send_welcome(user.email)
        return user

# AI 生成的测试(使用 pytest-mock)
import pytest
from unittest.mock import AsyncMock, MagicMock


@pytest.fixture
def user_service(mocker):
    db = MagicMock()
    db.users.exists = AsyncMock(return_value=False)
    db.users.create = AsyncMock(return_value=UserModel(
        id=1, email="test@example.com"
    ))
    email_service = MagicMock()
    email_service.send_welcome = AsyncMock()
    s3 = MagicMock()
    return UserService(db, email_service, s3)


@pytest.mark.asyncio
async def test_create_user_sends_welcome_email(user_service):
    """创建用户成功后应发送欢迎邮件"""
    user_data = UserCreateSchema(email="test@example.com", name="Test")
    await user_service.create_user(user_data)
    user_service.email_service.send_welcome.assert_called_once_with("test@example.com")


@pytest.mark.asyncio
async def test_create_user_duplicate_email_raises_error(user_service):
    """邮箱已存在时应抛出 DuplicateEmailError"""
    user_service.db.users.exists = AsyncMock(return_value=True)
    with pytest.raises(DuplicateEmailError):
        await user_service.create_user(
            UserCreateSchema(email="existing@example.com")
        )
    # 邮件不应被发送
    user_service.email_service.send_welcome.assert_not_called()

测试质量审核

用 AI 审核 AI 生成的测试

这是一个有趣但实用的做法:用 AI 来审核 AI 生成的测试,发现潜在的问题:

请审核以下测试代码的质量。

重点检查:
1. **测试意图清晰性**:测试名称是否准确描述了被测行为?
2. **过拟合实现**:测试是否依赖了实现细节而非期望行为?
3. **断言充分性**:断言是否足够?是否存在总是通过的"空断言"?
4. **测试独立性**:测试之间是否有隐藏的依赖顺序?
5. **边界覆盖**:是否遗漏了重要的边界条件?
6. **Mock 合理性**:Mock 的范围是否合适?是否 Mock 了不应该 Mock 的东西?

测试代码:
```python
{test_code}
```

对应的被测代码:
```python
{production_code}
```
测试的黄金标准

一个好的单元测试应该满足 FIRST 原则:Fast(运行快,毫秒级)、Independent(不依赖其他测试的执行顺序)、Repeatable(任何环境下结果一致)、Self-validating(Pass/Fail 明确,不需要人工判断)、Timely(在代码编写时或之前写测试)。

测试生成的技术原理与陷阱

AI 如何生成测试用例?

理解 AI 生成测试的机制,能帮助你判断生成结果的可信度:

等价类分析(Equivalence Class Partitioning)
AI 会将输入空间划分为若干"等价类",每类选一个代表测试。例如对 age: int 参数,AI 会识别出:负数(invalid)、零(boundary)、正常范围(1-150,valid)、超大值(invalid)四个等价类,各生成一个测试用例。这与人工分析方法论相同,但 AI 执行速度更快,且不容易遗漏明显的类别。
边界值分析(Boundary Value Analysis)
AI 特别善于找到 off-by-one 类型的边界:如果条件是 >= 18,AI 会生成 17、18、19 三个测试;如果是字符串长度限制 <= 50,会生成长度49、50、51 的测试。这些边界测试是人类最容易遗漏的,但对发现 Bug 最有价值。
文档字符串提取(Docstring Extraction)
当函数有详细的 docstring(特别是 Raises 部分),AI 会直接基于文档描述生成对应的测试:每个 Raises 条目对应一个异常测试,每个 Returns 描述对应一个正常路径测试。这就是为什么先写好 docstring 再让 AI 生成测试,效果远好于没有文档的函数。

Mock 的深层原理:什么时候该 Mock?

Mock 是测试中最容易误用的技术,理解其原理才能正确使用:

应该 Mock 的依赖:
  ✓ 外部服务(HTTP API、数据库、消息队列)
    原因:外部服务有延迟、不稳定、测试环境可能无法访问
  ✓ 系统时间(datetime.now())
    原因:时间相关逻辑需要在固定时间点测试
  ✓ 随机数生成器
    原因:测试必须可重复,随机行为会导致测试不稳定
  ✓ 文件系统(生产数据文件)
    原因:测试不应依赖实际文件的存在

不应该 Mock 的:
  ✗ 被测代码的内部实现细节
    危险:内部实现改变时测试失败,但代码行为没变(脆性测试)
  ✗ 简单的工具函数(纯函数)
    原因:Mock 它们比直接调用更复杂,且掩盖了这些函数的 Bug
  ✗ 整个服务层,仅仅为了"快速"
    危险:测试不再测试真实的集成逻辑,覆盖率数字虚高

测试覆盖率的正确理解

覆盖率是工具,不是目标:

行覆盖率 vs 分支覆盖率
行覆盖率(Line Coverage)统计哪些代码行被执行了;分支覆盖率(Branch Coverage)统计每个 if/else 的每个分支是否都被测试了。后者更严格:一行 return x if condition else y 有行覆盖但只有 50% 分支覆盖(只测了 condition=True 的情况)。AI 生成测试时,应指定"请确保 100% 分支覆盖"。
覆盖率 100% 不代表没有 Bug
即使覆盖了所有行和分支,测试仍可能不充分:断言不够强(只断言不报错,不验证输出值)、没有测试并发场景、没有测试极端输入(整型溢出、Unicode 特殊字符)。覆盖率是必要条件而非充分条件。
AI 生成测试的三大质量陷阱
第7章小结