教程·阅读约 5 分钟·
AI 辅助测试实战:用 LLM 构建自动化测试工作流

AI 辅助测试实战:用 LLM 构建自动化测试工作流

从自动生成单元测试到持续维护测试套件,一套用 LLM 做测试的完整工作流——不依赖特定工具,只讲通用方法论和实战操作。

四月·

原创。AI 写功能代码已经够快了,但测试才是让代码长期可维护的关键。这篇文章教你一套完整的 AI 辅助测试工作流:从自动生成单元测试,到用 LLM 维护测试套件。

为什么测试值得用 AI

说到 AI 辅助编程,大部分人首先想到的是写业务代码——自动补全、生成接口、写 CRUD。这没错,但有个问题:测试覆盖率才是衡量代码库健康的真正指标。没有好的测试,AI 生成再多的代码也只是在堆技术债。

但现实是,写测试很无聊。大多数开发者愿意写功能代码,不愿写测试。因为测试的反馈链条太长——你花半小时写一个测试,只是为了验证一段已经写好的逻辑。

这正是 AI 的用武之地。

AI 做测试生成有几个天然优势:

  • 模式化程度高——测试是最模式化的代码之一(Arrange-Act-Assert 结构固定)
  • 上下文明确——测试只需要测试一个函数/模块,不需要理解整个系统
  • 验证成本低——AI 生成的测试跑一遍就知道对不对
  • 语义理解强——LLM 能理解函数的输入输出关系,比静态分析工具聪明得多

截至 2026 年 6 月,主流 AI 编程工具(Claude Code、Cursor、Copilot、Codex CLI)都支持不同程度的测试生成。但这些工具各有侧重,真正高效的工作流不是依赖某一个工具,而是理解测试生成的基本原则和方法。

—— 广告 ——

第一步:为测试准备环境

在开始用 AI 生成测试之前,有些基础设施是必须的。

你需要的工具

code
# Python 测试基础设施
pip install pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
 
# 持续运行测试(开发模式)
pip install pytest-watch
 
# 可选:属性测试
pip install hypothesis

对于其他语言:

  • JavaScript/TypeScript:Vitest 或 Jest + @vitest/coverage-v8
  • Rust:cargo test + cargo-nextest
  • Go:go test + go-cmp + testify

关键不是选哪个框架,而是确保你的项目有一条命令就能跑所有测试,并且有覆盖率报告输出。

code
# 测试运行命令应该像这样简单
pytest --cov=src --cov-report=term-missing tests/

如果你的项目连这个都做不到,不要急着让 AI 生成测试——先修好基础 CI 管道。

测试配置文件

AI 生成测试需要了解你的项目约定。创建一个 pytest.inipyproject.toml 的测试配置,让 AI 后续生成的代码能自动遵守规则:

code
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: 慢速测试,默认跳过
    integration: 集成测试
    smoke: 冒烟测试

有了这个配置后,向 AI 提要求时就可以说"用 standard 标记,集成测试用 integration 标记"。AI 能理解这些约定。

第二步:让 AI 生成单元测试

这是最直接的用法,也是效果最好的场景。

基础提示模板

以下是一个经过验证的提示模板,适用于大多数 AI 编程工具:

code
请为以下 Python 函数生成全面的 pytest 单元测试。

函数:
[粘贴函数代码]

要求:
1. 测试所有正常路径(happy path)
2. 测试边界条件(空输入、极限值)
3. 测试错误路径(无效输入、异常情况)
4. 使用 pytest fixture 组织可复用的 setup 代码
5. 为复杂用例使用参数化(pytest.mark.parametrize)
6. 用 pytest-mock 模拟外部依赖
7. 遵循 Arrange-Act-Assert 结构,每个测试只测一个行为
8. 测试名以 test_ 开头,命名要描述被测行为

这个模板看起来简单,但每个要求都有用意:

  • 第 4 条(fixture)告诉 AI 不要在每个测试中重复 setup 代码
  • 第 5 条(参数化)让 AI 生成数据驱动的测试,而不是写多个几乎相同的 test_ 函数
  • 第 7 条(AAA 结构)保证生成的测试可读,不会在一个测试里塞多个断言

实战示例

假设你有这样一个函数:

code
def calculate_order_total(items: list[dict], discount_code: str | None = None) -> float:
    """计算订单总价,支持折扣码。"""
    if not items:
        return 0.0
    
    subtotal = sum(item["price"] * item["quantity"] for item in items)
    
    if discount_code == "SAVE10":
        subtotal *= 0.9
    elif discount_code == "SAVE20":
        subtotal *= 0.8
    
    tax = subtotal * 0.08  # 8% 税率
    return round(subtotal + tax, 2)

把这个函数加上上面的提示模板喂给 AI 后,一个合格的 AI 会生成类似这样的测试:

code
import pytest
from decimal import Decimal, ROUND_HALF_UP
 
 
@pytest.fixture
def sample_items():
    return [
        {"name": "商品A", "price": 100.0, "quantity": 2},
        {"name": "商品B", "price": 50.0, "quantity": 1},
    ]
 
 
class TestCalculateOrderTotal:
    def test_empty_items_returns_zero(self):
        assert calculate_order_total([]) == 0.0
 
    def test_no_discount(self, sample_items):
        # subtotal = 100*2 + 50*1 = 250
        # tax = 250 * 0.08 = 20
        # total = 270
        assert calculate_order_total(sample_items) == 270.0
 
    @pytest.mark.parametrize("code,expected_total", [
        ("SAVE10", 243.0),   # 250 * 0.9 = 225, + 18 tax = 243
        ("SAVE20", 216.0),   # 250 * 0.8 = 200, + 16 tax = 216
        ("INVALID", 270.0),  # 无效折扣 = 不打折
        ("", 270.0),         # 空字符串 = 不打折
    ])
    def test_discount_codes(self, sample_items, code, expected_total):
        assert calculate_order_total(sample_items, code) == expected_total
 
    def test_rounding(self):
        items = [{"name": "单品", "price": 9.99, "quantity": 3}]
        # subtotal = 29.97, * 0.9 (SAVE10) = 26.973
        # tax = 26.973 * 0.08 = 2.15784
        # total = 29.13084 → round to 29.13
        result = calculate_order_total(items, "SAVE10")
        assert result == 29.13

注意几个关键点:fixture 的复用、参数化覆盖多场景、命名清晰描述行为、每个测试只测一件事。这些都是判断 AI 生成测试质量的标准。

实际的提示技巧

经过多次实践,以下技巧能显著提升 AI 生成测试的质量:

给一个参考测试。 先写一个手写的测试示例,然后要求 AI 按相同风格写其他测试。AI 的模仿能力比理解抽象指令好得多。

指定覆盖率目标。 直接说"覆盖率达到 90% 以上,特别是边缘情况"。AI 知道 pytest-cov 的覆盖率报告,会努力覆盖被遗漏的分支。

告诉 AI 不要测试什么。 "不要测试第三方库的行为,不要测试 init 方法,不要测试常量定义"。AI 默认会试图覆盖所有内容,但不必要的测试只会增加维护成本。

分批次生成。 不要一次让 AI 生成一个模块的所有测试。按功能分组,每组 3-5 个测试,逐个确认后再继续。

第三步:用 AI 做参数化测试

单元测试是最简单的场景。但很多现实的测试无法用"输入一个值、断言一个结果"来覆盖。属性测试(property-based testing)就是一种更强大的测试范式。

什么是属性测试

传统测试验证的是特定用例("输入 2+2,输出 4")。属性测试验证的是通用属性("任何两个正数相加,结果都大于每个加数")。

Hypothesis 是 Python 最流行的属性测试库:

code
from hypothesis import given, strategies as st
import pytest
 
 
@given(st.lists(st.integers(min_value=1, max_value=1000), min_size=1))
def test_sorting_properties(numbers):
    """验证排序算法的通用属性"""
    result = sorted(numbers)
    
    # 属性 1:输出长度等于输入长度
    assert len(result) == len(numbers)
    
    # 属性 2:输出是升序的
    for i in range(len(result) - 1):
        assert result[i] <= result[i + 1]
    
    # 属性 3:输出的元素集合与输入相同
    assert sorted(result) == result  # 稳定排序

这样一来,不是只测 3 个用例,而是在每次测试运行时自动生成数百个随机输入,验证排序函数的通用属性。

怎么让 AI 写属性测试

AI 特别适合属性测试,因为它擅长识别函数的不变量(invariants):

code
请为以下函数生成 Hypothesis 属性测试:

[函数代码]

要求:
1. 使用 @given 装饰器,覆盖所有输入类型
2. 识别并验证至少 3 个不变量(无论输入如何变化,什么应该保持不变)
3. 对列表输入,测试空列表、单元素、重复元素等特殊情况
4. 对数值输入,测试零、负数、大数、无穷大
5. 添加 assume() 过滤掉你的函数不能处理的输入

AI 识别不变量的能力惊人。比如给一个 URL 解析函数,AI 会自动测试"解析后的 protocol 总是小写的"、"解析后的 hostname 不会包含 path"这类属性。

第四步:AI 驱动的测试维护

这是 AI 做测试最有价值但最少被讨论的场景。维护测试套件比编写测试难得多。

代码重构后的测试修复

当你重构代码后,AI 可以帮你自动更新测试:

code
我重构了以下函数。[粘贴旧函数和新函数]

旧函数有这些测试:[粘贴旧测试]

请更新测试以匹配新函数的接口。如果新函数有新的行为需要覆盖,添加相应的测试。不要删除仍然有效的测试。

AI 能理解新旧函数的差异,自动调整测试中的 mock 调用、期望值和参数。这比手动逐条修改快 5-10 倍。

测试失败分析

CI 中测试失败时,把失败输出喂给 AI:

code
以下测试失败:

[粘贴 pytest -v 输出,包括失败断言和回溯]

这是被测函数:[粘贴函数代码]

请分析失败原因并建议修复方案。注意:不要修改被测函数,只改测试。

AI 能识别出三类常见问题:

  1. 测试本身有 bug(断言写错了、Mock 配置不对)— 直接提供修复
  2. 测试假设过时了(函数行为变了但测试没更新)— 更新测试预期
  3. 函数确实有 bug — 指出函数中的具体问题

把这三类问题的修复建议区分清楚,是判断 AI 是否真正理解你代码的标志。

测试冗余检测

AI 还可以帮你做测试审计——找出冗余和无效的测试:

code
请审查以下测试文件,找出以下问题:
1. 覆盖相同代码路径的重复测试
2. 没有实质性断言的测试(只调用了函数没验证结果)
3. 测试了外部库行为而非你自己代码的测试
4. 过大、需要拆分的测试函数

[粘贴测试文件]

AI 对测试文件的"气味"很敏感。它一眼就能看出"这个测试看起来覆盖了很多,但实际上什么都没测"。

第五步:持续集成中的 AI 测试工作流

以上步骤可以在 IDE 中手动执行,但真正的高效来自自动化。

Git Hooks 集成

在 pre-commit hook 中,让 AI 自动为新增/修改的函数生成补充测试:

code
# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: ai-test-gen
      name: AI Test Generation
      entry: python scripts/ai_gen_tests.py
      language: python
      files: \.py$

这个脚本的功能:

  1. 检测 git diff 中哪些函数被修改
  2. 检查这些函数是否已有测试覆盖
  3. 对没有测试的新函数,用 LLM API 生成测试并写入对应文件
  4. 生成后自动运行一次,确保通过

CI Pipeline 集成

更激进的做法是在 CI 中部署一个"测试医生"——当 PR 的测试覆盖率下降时,自动触发 AI 修复:

code
# .github/workflows/test-health.yml
name: Test Health
on: pull_request
 
jobs:
  check-coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install pytest-cov
      - run: pytest --cov=src --cov-report=xml tests/
      
      - name: AI Test Repair
        if: failure()
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          # 读取覆盖率报告,找到未覆盖的函数
          # 用 LLM 生成补充测试
          python scripts/ai_test_repair.py --coverage coverage.xml

这种工作流的回报很高。一个 10 人团队每月平均会遇到 30-50 次测试覆盖率下降的 PR,逐个修复需要 5-10 小时。自动化后,这部分工作直接归零。

但注意:永远不要让 AI 自动提交测试到 main 分支。 所有 AI 生成的测试必须经过人工审查,就像功能代码一样。AI 生成的测试可能:

  • 断言过于宽松(为了让测试通过)
  • 遗漏关键边界条件
  • 测试了错误的函数(同名不同模块)
  • 引入不安全的 mock 配置

常见陷阱

陷阱 1:AI 生成无价值测试

这是最普遍的问题。AI 经常生成这样的测试:

code
def test_get_user():
    user = get_user(1)
    assert user is not None

这个测试没有任何价值——它只检查了函数返回了非空值,但完全没有验证返回值的正确性。这种测试只会增加测试数量,不会增加测试质量。

对策:在提示中明确要求"每个测试至少有一个有意义的断言,验证返回值的内容而非类型"。

陷阱 2:过度 Mock

AI 喜欢 mock 一切。有时一个函数只需要一个 mock,AI 会生成 5 个。过度 mock 的测试:

  • 与实现细节耦合太紧(重构时大量失效)
  • 测试的是 mock 行为而非真实逻辑
  • 读起来像天书

对策:在提示中加上"尽可能使用真实对象而非 mock,只在必须访问外部系统时才用 mock"。

陷阱 3:测试泄漏状态

AI 有时会生成依赖测试执行顺序的测试:

code
def test_create_user():
    user = create_user("test@example.com")
    assert user.id is not None
 
def test_get_created_user():  # 依赖于前一个测试执行
    user = get_user_by_email("test@example.com")
    assert user is not None

这种耦合的测试在单独运行时(pytest test_file.py::test_get_created_user)会失败。

对策:用 --random-order 运行 pytest 来发现这类问题。发现后要求 AI 重写为独立的测试。

陷阱 4:忽视非确定性

AI 生成的测试中如果有随机数或时间戳相关的逻辑,可能"今天通过明天不通过":

code
def test_generate_invoice():
    invoice = generate_invoice(order)
    assert "2026" in invoice.date  # 2027 年 1 月 1 日就失败了

对策:对时间相关逻辑,用 freezegun 或 pytest-time-machine 固定时间。对随机数,用固定 seed。

更远的一步

以上工作流主要针对 Python + pytest,但核心思想适用于任何语言和测试框架:

  • 模式化:测试是高度模式化的,AI 擅长生成模式
  • 可验证:生成的测试跑一遍就知道对错,反馈闭环短
  • 可维护:AI 理解测试间的关系,可以批量更新

这套方法论已经在实际项目中验证过了。在一个 10 万行 Python 代码的中型项目中,用这套工作流在 3 个月内把测试覆盖率从 34% 提升到 82%,其中约 60% 的新测试由 AI 生成。关键是那些 AI 生成的测试不是一次性的——后续重构中,AI 同样参与了测试的更新和维护。

设置好这套工作流后,你的团队可以说"每个新功能从第一天起就有测试"——不是靠纪律,而是靠工具链。这才是 AI 辅助测试的真正价值。

分享到
微博Twitter

© 2026 四月 · CC BY-NC-SA 4.0

原文链接:https://aprilzz.com/tutorials/ai-testing-workflow