游乐游手机版
首页/AI教程/文章详情

OpenSpec+TDD实践:AI写代码的测试兜底方案

时间:2026-06-06 17:15
AI 写代码确实效率惊人,但你有没有问过它一个关键问题:“你写的代码真的正确吗?”它会自信地回答“全部通过”,可一运行,全是错误。这种表面上的完美,往往隐藏着最致命的隐患。下面这套方案,用 OpenSpec 来梳理和管理需求定义,用 TDD(测试驱动开发)来保证代码的正确性,两者在实际开发中串联使用

AI 写代码确实效率惊人,但你有没有问过它一个关键问题:“你写的代码真的正确吗?”它会自信地回答“全部通过”,可一运行,全是错误。这种表面上的完美,往往隐藏着最致命的隐患。下面这套方案,用 OpenSpec 来梳理和管理需求定义,用 TDD(测试驱动开发)来保证代码的正确性,两者在实际开发中串联使用,效果非常理想。

OpenSpec + TDD:让 AI 写代码,用测试兜底

为什么必须这样组合

如今越来越多开发者在大量使用 AI 辅助编码,效率确实呈爆发式提升。但运行一段时间后,会察觉到一个令人不安的现象:AI 生成的代码看起来总是滴水不漏。

它不会主动报错,也不会说“这里我不确定”,写出的代码结构清晰、命名规范、注释齐全——简直就是模范代码。可一旦实际运行呢?边界情况被遗漏了,业务逻辑理解出现了偏差,有时甚至会编造一个根本不存在的 API,还自信满满地去调用。你问它“问题解决了吗”,它回答“全部检测通过已经完成”,其实连编译都通不过。

这种现象被称为 AI 的“自洽幻觉”:它总能给出一个看似“合理”的答案,但合理从来不代表正确。

因此核心问题变成了:在代码生成成本越来越低的今天,正确性才是真正稀缺的资源。谁来保障正确性?测试。注意,不是写完代码后补的那种测试,而是 TDD 那种——先写测试,再让 AI 去实现。

但仅有 TDD 还不够。测试能告诉你“代码能不能跑对”,却无法告诉你“需求本身拆分得是否合理”。如果需求拆分歪了,即使所有测试都绿,也是白费功夫。

这就是 OpenSpec 的定位:它负责“做什么”,TDD 负责“做对没”。

整体流程概览

两者的衔接点落在 Apply 阶段。前期用 OpenSpec 把意图理清楚、把方案定下来,到了动手环节,每个 task 内部走 TDD 循环。

Explore → Propose → Apply (TDD循环) → Archive
│          │           │                │
│思考讨论  │proposal.md│ Red→Green→Refactor│归档 + sync specs
│明确意图  │design.md  │ 逐task推进      │
│          │tasks.md   │                │
│          │specs/     │                │

四个阶段各司其职,但并不是僵化的瀑布流——你随时可以从 Apply 退回 Propose 去修改设计,也可以跳过 Explore 直接开始。关键在于每个阶段解决不同的问题。

Tasks 的正确写法

这是整个方案中最关键的一环。常见的 task 写法是“实现用户登录功能”——这种粒度对 AI 来说过于粗糙,它会一口气把登录、注册、token 生成全部写完,测试?根本不存在。

正确做法是将测试 task 和实现 task 成对出现:

## Tasks
- [ ] 为用户登录写失败路径测试(无效密码、不存在用户)
- [ ] 实现登录逻辑使测试通过
- [ ] 为token生成写测试(过期、签名验证)
- [ ] 实现token生成使测试通过
- [ ] 重构:提取认证中间件
- [ ] 为中间件写集成测试
- [ ] 实现中间件使测试通过

先测试、再实现、中间穿插 refactor。这并非形式主义——这个顺序决定了 AI 在 Apply 阶段的行为模式。如果你不显式写出测试 task,AI 会直接跳到实现,然后补上一堆 happy path 测试来交差。

Apply 阶段的 TDD 节奏

每个 task 内部遵循经典的 Red-Green-Refactor 循环:

单个 Task 的执行流程:

1. Red → 写测试,运行,确认失败
2. Green → 写最少代码让测试通过
3. Refactor → 清理代码,测试仍通过
4. ✓ 标记task完成

有一个细节特别容易被忽略:第 1 步的“确认失败”非常关键。假如你一上来测试就绿了,那说明两种情况——要么这个功能已经实现了(那这个 task 就是多余的),要么你的测试写得有问题(没测到关键点上)。无论哪种情况,都必须停下来仔细分析。

“写最少代码”也有讲究。AI 特别喜欢过度实现——你让它通过一个测试,它顺手把后面三个 task 的功能也写了。此时需要在 prompt 里明确约束:只让当前这一个测试变绿,不要多做。

实际操作完整演示

拿“给系统加用户认证”这个需求举例:

# 1. Explore - 理清要做什么
/opsx:explore add-user-auth

# 讨论:用JWT还是session?是否需要第三方登录?密码策略如何制定?

# 2. Propose - 生成方案(task按TDD节奏拆分)
/opsx:propose add-user-auth

# 产出 proposal.md / design.md / tasks.md / specs

# 3. Apply - 逐个task实施
/opsx:apply add-user-auth

Apply 过程中,AI 的实际行为演示:

// Task: "为用户登录写失败路径测试"
// AI写出:
@Test
fun `login with invalid password returns error`() = runTest {
    val testUser = userRepository.createUser("test@example.com", "correct_password")
    val result = loginUseCase.execute("test@example.com", "wrong_password")
    assertThat(result).isInstanceOf(LoginResult.Failure::class.ja va)
    assertThat((result as LoginResult.Failure).reason).isEqualTo("invalid_credentials")
}

@Test
fun `login with nonexistent user returns error`() = runTest {
    val result = loginUseCase.execute("nobody@example.com", "any_password")
    assertThat(result).isInstanceOf(LoginResult.Failure::class.ja va)
}

// → 运行测试 → RED ✗(因为LoginUseCase尚未实现)
// → 标记为已完成 [x]

// Task: "实现登录逻辑使测试通过"
// AI写出:
class LoginUseCase @Inject constructor(
    private val userRepository: UserRepository,
    private val tokenManager: TokenManager
) {
    suspend fun execute(email: String, password: String): LoginResult {
        val user = userRepository.findByEmail(email) ?: return LoginResult.Failure("invalid_credentials")
        if (!user.verifyPassword(password)) {
            return LoginResult.Failure("invalid_credentials")
        }
        val token = tokenManager.createAccessToken(user.id)
        return LoginResult.Success(token)
    }
}

// → 运行测试 → GREEN ✓
// → 标记为已完成 [x]

# 4. 所有task完成后归档
/opsx:archive add-user-auth

Spec 与 Test 的关系

这两者的关系容易混淆,用一个类比说清楚:Spec 是合同,Test 是验收。

合同写的是“用户连续5次登录失败应锁定账户”,这是业务语言,给人看的。验收做的是 account locks after 5 failures 这个测试用例,这是代码语言,给机器跑的。

openspec/specs/auth/spec.md ← 合同(业务语言)
“用户连续5次登录失败应锁定账户”

app/src/test/ja va/.../auth/LoginUseCaseTest.kt ← 验收(代码语言)
fun `account locks after 5 consecutive failures`()

每一条 spec 都应该能映射到至少一个测试——如果有一条 spec 找不到对应的测试,要么是测试遗漏了,要么是这条 spec 写得过于空泛(比如“系统应该安全”这种就无法直接测试)。

反过来也成立:如果你写了一个测试却找不到对应的 spec,说明你在测试一个没有被明确定义过的行为。这种测试不是不能存在,但需要思考它是否应该先变成一条 spec。

推荐 config.yaml 配置

schema: spec-driven

context: |
  开发模式:OpenSpec + TDD
  测试框架:JUnit5 + Truth + Mockk
  # Android项目常用组合
  测试目录:app/src/test/ 和 app/src/androidTest/
  每个task必须先有测试覆盖再写实现

rules:
  tasks:
    - 测试类task和实现类task必须成对出现
    - 每个实现task之前必须有对应的测试task
    - Refactor task的前提是所有测试通过
  design:
    - 设计中需包含testability考虑
    - 标注哪些边界需要mock/stub

这个配置的作用是约束 AI 在 Propose 阶段生成 tasks 时严格遵循 TDD 节奏。如果没有这些 rules,AI 很容易生成“实现 XX 功能”这样笼统的 task,导致测试被完全忽略。

这套方案实际使用下来,最直观的体会不是代码质量大幅提升,而是终于可以在 AI 写完代码之后安心去喝杯咖啡了。测试守住了底线,spec 锁住了意图,即使 AI 出现幻觉,也逃不开这个圈。

来源:https://juejin.cn/post/7623442090362585122
上一篇2027人类最终抉择 下一篇OpenClaw火爆背后的技术人不服与深思
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Synthesia零基础教程:客户端安装与工作区权限设置
AI教程 · 2026-06-07

Synthesia零基础教程:客户端安装与工作区权限设置

本文介绍了AI视频生成工具Synthesia的入门流程。内容涵盖从官网下载客户端、完成账户注册与登录,到软件安装与启动的完整步骤。详细说明了如何初始化工作区,包括创建首个AI视频项目、选择模板与AI主播。最后,指导用户理解并设置团队协作中的不同权限角色,以便安全高效地共同管理项目。

FramePack新手入门指南:安装启动报错修复导出全流程
AI教程 · 2026-06-07

FramePack新手入门指南:安装启动报错修复导出全流程

本文详细介绍了FramePack工具从下载安装到项目导出的完整流程。内容涵盖软件安装步骤、首次启动设置、常见报错解决方案以及项目打包导出方法。指南旨在帮助用户快速掌握工具核心操作,解决使用过程中可能遇到的技术问题,确保顺利完成AI视频帧处理任务。

FLUX.1保姆级教程:环境安装、显存优化与首次出图测试
AI教程 · 2026-06-07

FLUX.1保姆级教程:环境安装、显存优化与首次出图测试

本文详细介绍了FLUX 1的安装与初步使用流程。内容涵盖从Python环境配置、代码仓库克隆、依赖包安装,到关键的显存优化设置,最后指导用户完成首次文生图测试。教程旨在帮助用户顺利搭建运行环境,解决常见安装问题,并实现基础图像生成功能。

AnythingLLM新手实战:本地大模型部署后知识库接入设置
AI教程 · 2026-06-07

AnythingLLM新手实战:本地大模型部署后知识库接入设置

本文介绍了在本地部署大模型后,如何为AnythingLLM设置知识库。内容涵盖知识库的基本概念、创建与配置步骤、文档上传与处理技巧,以及如何通过问答测试其效果。旨在帮助用户有效整合本地文档资源,构建个性化的AI知识助手,提升信息检索与利用效率。

Aider安装失败排查:扩展冲突与登录异常全解析
AI教程 · 2026-06-07

Aider安装失败排查:扩展冲突与登录异常全解析

本文针对Aider安装过程中常见的扩展冲突与登录异常问题,提供了系统的排查思路与解决方案。内容涵盖如何识别并处理与其他AI工具的兼容性问题,解决因网络或账户设置导致的登录失败,以及通过环境检查、依赖更新等步骤彻底排除安装障碍,帮助用户顺利完成安装与配置。