乐于分享
好东西不私藏

AI原生开发:ClaudeCode与国产大模型协同实战 第7章:AI驱动的契约式TDD——做团队最严格的守门人

AI原生开发:ClaudeCode与国产大模型协同实战 第7章:AI驱动的契约式TDD——做团队最严格的守门人

老兵们,聊到测试,咱们交个底:你有多讨厌写单元测试?别装了,我知道大部分人的真实状态是:代码写得飞起,测试全靠手点。 为什么?因为写单元测试太反人类了。你要去Mock各种依赖,要构造各种奇奇怪怪的边界数据,为了测一个几行的核心逻辑,可能要写几十行的SetUp。投入产出比太低,上线一催,测试自然就砍了。但这带来了无穷无尽的锅:线上一点点小改动,把老逻辑搞崩了,半夜爬起来修Bug。今天,我们要彻底颠覆这个局面。让人去写业务代码,让AI去写测试。而且,我们要用最腹黑的方式,把AI逼成一个比你还严苛的守门员。

7.1 TDD的终极形态:用OpenSpec定义行为,先有规约,后有代码

传统的TDD(测试驱动开发)是个美好的童话:先写测试 -> 跑红 -> 写业务代码 -> 跑绿 -> 重构。但现实中,业务需求都排到下个月了,谁有空先写测试?但如果我们把TDD和OpenSpec结合起来,就形成了契约式TDD(Contract-Driven TDD)这改变了游戏的玩法。你不需要先写测试代码,你只需要先写契约。只要契约定下来了,AI就能瞬间根据契约生成覆盖所有异常分支的测试用例,然后再生成让测试通过的业务代码。

核心逻辑:OpenSpec里的每一个 pre_condition 和 contract.error,都必须对应一个测试用例。 这样,测试就不再是可有可无的附属品,而是契约的具象化守护者。

7.2 【实操】GLM-5.1自动生成高覆盖率单元测试与集成测试

我们来实战一把。假设我们要开发一个极其常见的电商功能:代金券核销按照契约式TDD的流程,第一步不是写代码,而是写规约。我们定义一个极简的OpenSpec契约:

# voucher_spec.yaml (精简版)contracts:  RedeemVoucherAPI:    endpoint: /voucher/redeem    method: POST    request:      voucher_id: { type: stringrequiredtrue }      order_id: { type: stringrequiredtrue }      order_amount: { type: floatrequiredtrue, minimum: 0.01 }    response:      success:        code: 200        data: { deducted_amount: float }      errors:        - code: 40401          message: "Voucher not found"        - code: 40402          message: "Voucher already redeemed"        - code: 40403          message: "Order amount does not meet minimum requirement"
第二步,把这份契约喂给GLM-5.1,让它生成 pytest 测试代码。👉 请在ClaudeCode中输入以下指令:
# Role你是一个极其严苛的测试工程师,信奉契约精神。# Task请根据提供的 OpenSpec 规约,使用 Python + Pytest 生成单元测试代码。# OpenSpec 规约(粘贴上面的 voucher_spec.yaml)# Directive1. 必须覆盖所有 contracts.errors 中的异常码(40401, 40402, 40403)。2. 必须覆盖成功的正常路径(200)。3. 业务逻辑类 VoucherService 暂时不存在,请使用 unittest.mock 的 MagicMock 来模拟依赖(如数据库查询)。4. 测试类名和方法名必须能清晰表达意图,不能使用 test_1, test_2 这种无意义命名。请直接输出 Pytest 测试代码。
你会看到,AI毫不留情地把所有异常分支都写成了测试。下面是生成的、直接能跑通的测试代码:📁 test_voucher.py
import pytestfrom unittest.mock import MagicMock# 模拟的业务异常类(根据OpenSpec生成)class VoucherError(Exception):    def __init__(self, code, message):        self.code = code        self.message = message# 模拟的业务服务类(待后续实现)class VoucherService:    def redeem(self, voucher_id: str, order_id: str, order_amount: float) -> dict:        pass # 待实现@pytest.fixturedef mock_db():    return MagicMock()@pytest.fixturedef service(mock_db):    return VoucherService(db=mock_db)class TestVoucherRedeem:    """代金券核销契约测试"""    def test_redeem_success(self, service, mock_db):        """正常核销成功"""        mock_voucher = MagicMock(id="V1", is_redeemed=False, min_requirement=50.0, face_value=10.0)        mock_db.get_voucher.return_value = mock_voucher        result = service.redeem("V1""O1"100.0)        assert result["code"] == 200        assert result["data"]["deducted_amount"] == 10.0    def test_redeem_voucher_not_found(self, service, mock_db):        """异常40401: 代金券不存在"""        mock_db.get_voucher.return_value = None        with pytest.raises(VoucherError) as exc_info:            service.redeem("V_NONEXIST""O1"100.0)        assert exc_info.value.code == 40401    def test_redeem_already_redeemed(self, service, mock_db):        """异常40402: 代金券已核销"""        mock_voucher = MagicMock(id="V1", is_redeemed=True, min_requirement=50.0, face_value=10.0)        mock_db.get_voucher.return_value = mock_voucher        with pytest.raises(VoucherError) as exc_info:            service.redeem("V1""O1"100.0)        assert exc_info.value.code == 40402    def test_redeem_order_amount_insufficient(self, service, mock_db):        """异常40403: 订单金额不满足最低要求"""        mock_voucher = MagicMock(id="V1", is_redeemed=False, min_requirement=50.0, face_value=10.0)        mock_db.get_voucher.return_value = mock_voucher        with pytest.raises(VoucherError) as exc_info:            service.redeem("V1""O1"30.0# 30.0 < 50.0        assert exc_info.value.code == 40403

这就是契约式TDD的威力。测试先行,且每一条测试都死死咬住规约。现在你可以放心地去让AI写 VoucherService.redeem 的实现了,只要跑绿这四个用例,你的代码就不会跑偏。

7.3 【深挖】Mock与边界条件:教AI想得比你更腹黑

上面的测试能防住常规Bug,但这还不足以让你成为团队里最让人闻风丧胆的守门员。资深工程师都知道,线上跑崩的代码,80%不是正常逻辑写错了,而是遇到极端的脏数据、网络超时、并发冲突时,没做防御。AI默认生成的测试,往往也是“阳光思维”——测测正常路径,测测常规报错。想让它变得“腹黑”,你必须给它戴上“混蛋面具”怎么戴?在Prompt里加入Fault Injection(故障注入)Boundary Testing(边界测试)的强制约束。

实战演示:逼AI写出防脏数据的测试我们在上面的测试需求中,追加一段极其狠毒的Prompt:

“…除了上述规约,请补充 3 个极其腹黑的边界测试用例:1. 当 voucher_id 包含SQL注入片段(如 “V1′; DROP TABLE vouchers;”)时,系统不能抛出未捕获异常,必须返回40401。2. 当 order_amount 为负数(如 -10.0)时,必须返回参数校验错误(假设错误码400)。3. 当底层数据库查询 (mock_db.get_voucher) 突然抛出连接超时异常 时,服务层必须捕获并返回50001错误码,绝不能把异常堆栈抛给上层。”AI拿到这个指令,就会瞬间化身黑客,生成这样的补充测试:

📁 test_voucher_evil.py (追加的腹黑测试)
import pytestfrom unittest.mock import MagicMockfrom connection_timeout import ConnectionTimeoutError # 假设的DB异常# ... (复用前面的 VoucherService 和 VoucherError 定义)class TestVoucherRedeemEvil:    """代金券核销:腹黑边界与故障注入测试"""    def test_redeem_sql_injection_attempt(self, service, mock_db):        """防SQL注入:voucher_id包含恶意片段"""        mock_db.get_voucher.return_value = None        # 传入恶意ID,不应抛出未捕获异常,应视为找不到        with pytest.raises(VoucherError) as exc_info:            service.redeem("V1'; DROP TABLE vouchers;""O1"100.0)        assert exc_info.value.code == 40401    def test_redeem_negative_order_amount(self, service, mock_db):        """参数防御:订单金额为负数"""        with pytest.raises(VoucherError) as exc_info:            service.redeem("V1""O1", -10.0)        # 假设我们在Service层加了参数校验,返回400        assert exc_info.value.code == 400     def test_redeem_db_connection_timeout(self, service, mock_db):        """依赖容灾:数据库查询超时"""        # 模拟数据库突然抽风        mock_db.get_voucher.side_effect = ConnectionTimeoutError("DB timed out")        with pytest.raises(VoucherError) as exc_info:            service.redeem("V1""O1"100.0)        # 业务层必须兜底,不能把底层数据库超时暴露给前端        assert exc_info.value.code == 50001

深挖时刻:为什么这种测试才是有价值的?因为这才是资深工程师真正在防的东西。写 if-else 谁不会?但能在数据库挂掉的时候,还能给前端返回一个体面的降级提示,而不是让网关直接报500,这才是功力。当你要求AI根据这种“混蛋面具”Prompt去生成测试,然后再让AI自己去写业务代码让这些测试跑绿时,你会发现,AI写出的防御性代码,比你手写的还要严丝合缝。 它会把所有入参都做类型和边界校验,会在调用DB的地方裹上厚厚的 try-except

这,就是AI驱动契约式TDD的终极奥义:用最恶毒的测试倒逼出最健壮的代码。


🏆 第7章代码角斗场:互相伤害

这章的角斗场,咱们玩点刺激的“互相伤害”。

  1. 你写一个只有你自己知道的“毒药逻辑”(比如:如果用户名是admin,返回特殊错误码;如果金额是99.99,走特殊通道),不要告诉AI这个逻辑

  2. 让AI只根据你提供的“正常版”OpenSpec生成测试代码。

  3. 运行AI生成的测试,看看它能拦截住你的“毒药”吗?(大概率拦不住,因为AI不知道潜规则)。

  4. 现在,戴上混蛋面具,让AI补充5个“腹黑测试”,看看它能不能瞎猫碰上死耗子,测出你的潜规则!下一章,我们将从代码层面的防守,走向系统层面的排雷——当线上日志疯狂报红时,如何让AI一秒定位Root Cause?我们进入性能与调试的深水区!