AI TDD 开发方式初体验

需求澄清

猜数字游戏的规则包括:

  • 输入4个0~9中不同的数字,按enter键查阅结果是否正确(以“?A?B”形式显示)
    说明: ?A表示所输入的?个数字和位置都与手机的答案相同; ?B表示有?个数字相同而位置有误。例如,输入“3609”时显示为“1A2B ”表示其中有一个数的数字和位置都对了;有两个数的数字对但位置不对;还有一个数的数字和位置都不对。
  • 猜中数字,显示“4A0B”,游戏结束;
  • 如果6次没猜中,那么游戏失败,显示“You are lose”。

故事拆分

编号 描述
故事 1 作为玩家,我想要知道输入数中数字和位置都相同的个数,以便于赢得游戏
故事 2 作为玩家,我想要知道输入数中数字相同而位置有误的个数,以便于赢得游戏
故事 3 作为游戏设计者,我想要控制玩家尝试次数,以便于增加游戏的趣味性
故事 4 作为游戏设计者,我想要记录玩家每一次的猜数结果,以便于查看猜数历史

环境搭建

安装 Cursor

Cursor官网下载最新版本,本地安装完成后在菜单 Cursor / About Cursor 查看版本号:

Version: 0.47.8
VSCode Version: 1.96.2
Commit: 82ef0f61c01d079d1b7e5ab04d88499d5af500e0
Date: 2025-03-18T05:28:47.245Z
Electron: 32.2.6
Chromium: 128.0.6613.186
Node.js: 20.18.1
V8: 12.8.374.38-electron.0
OS: Darwin x64 20.6.0

规则配置

Cursor Settings / Rules 添加 AI TDD 开发的规则:

  • tdd-rule.mdc
# AI TDD 开发流程

- 根据用户故事或任务生成验收准则
- 根据验收准则生成测试代码
- 生成产品代码
- 测试成功(如果测试失败,则自动修复,直到成功,最多尝试3次)
- 重构代码:消除重复,代码易于理解,没有冗余
- 测试成功(如果测试失败,则自动修复,直到成功,最多尝试3次)
  • tcode-rule.mdc
# 测试代码生成规则
请你作为TDD专家级程序员,按照以下要求生成测试代码:
- 为当前用户故事所有的验收准则编写对应的测试代码
- 不要写更多的测试代码,避免过度设计
- 测试代码要求是GWT格式(given-when-then三段式)
- 使用goconvey测试框架

示例:
```go
import (
    . "github.com/smartystreets/goconvey/convey"
)

Convey("testEmptyManagerId", t, func() {
    Convey("given ", t, func() {
        Convey("when ", t, func() {
            Convey("then ", t, func() {
    
            })
        })
    })
})

mode 配置

Agent + claude-3.7-sonnet

AI TDD 实践

为了控制文章的规模,本文仅展示用AI TDD 开发故事1的过程。

故事一提示词

新建 Chat 页面,选择 tdd-rule 和 tcode-rule,并附上提示词:


prompt.png

prompt 文本如下:

## 需求澄清

猜数字游戏的规则包括:
- 输入4个0~9中不同的数字,按enter键查阅结果是否正确(以“?A?B”形式显示)
说明: ?A表示所输入的?个数字和位置都与手机的答案相同; ?B表示有?个数字相同而位置有误。例如,输入“3609”时显示为“1A2B ”表示其中有一个数的数字和位置都对了;有两个数的数字对但位置不对;还有一个数的数字和位置都不对。
- 猜中数字,显示“4A0B”,游戏结束;
- 如果6次没猜中,那么游戏失败,显示“You are lose”。

## 故事拆分
| 编号 | 描述  | 
|------|------|
| 故事 1  | 作为玩家,我想要知道输入数中数字和位置都相同的个数,以便于赢得游戏   | 
| 故事 2  | 作为玩家,我想要知道输入数中数字相同而位置有误的个数,以便于赢得游戏   | 
| 故事 3  | 作为游戏设计者,我想要控制玩家尝试次数,以便于增加游戏的趣味性   | 
| 故事 4  | 作为游戏设计者,我想要记录玩家每一次的猜数结果,以便于查看猜数历史   | 

## 指令
- 请以TDD的方式完成故事1开发,暂不考虑故事2、故事3和故事4
- 代码工程名为guess-number,外部API调用为 restult = game.Guess(number)
- 不要编写main.go文件和README.md文档

验收准则生成

acceptance.png

测试代码生成

tcode.png

game_test.go 代码如下:

package game

import (
    "testing"

    . "github.com/smartystreets/goconvey/convey"
)

func TestGuessNumber(t *testing.T) {
    Convey("故事1:计算输入数中数字和位置都相同的个数", t, func() {
        Convey("给定一个答案是1234的游戏", func() {
            game := NewGame("1234")

            Convey("当输入数字5678(完全不同)时", func() {
                result := game.Guess("5678")
                
                Convey("则返回的A值应为0", func() {
                    So(result, ShouldEqual, "0A0B")
                })
            })

            Convey("当输入数字1567(第一位相同)时", func() {
                result := game.Guess("1567")
                
                Convey("则返回的A值应为1", func() {
                    So(result, ShouldEqual, "1A0B")
                })
            })

            Convey("当输入数字1537(第一位和第三位相同)时", func() {
                result := game.Guess("1537")
                
                Convey("则返回的A值应为2", func() {
                    So(result, ShouldEqual, "2A0B")
                })
            })

            Convey("当输入数字1234(全部相同)时", func() {
                result := game.Guess("1234")
                
                Convey("则返回的A值应为4", func() {
                    So(result, ShouldEqual, "4A0B")
                })
            })
        })
    })
}

产品代码生成

pcode.png

game.go 代码如下:

package game

import "fmt"

// Game 表示一个猜数字游戏
type Game struct {
    answer string
}

// NewGame 创建一个新的猜数字游戏
func NewGame(answer string) *Game {
    return &Game{
        answer: answer,
    }
}

// Guess 猜测一个数字并返回结果
func (g *Game) Guess(number string) string {
    a := 0

    // 计算A的值(位置和数字都相同)
    for i := 0; i < len(number) && i < len(g.answer); i++ {
        if number[i] == g.answer[i] {
            a++
        }
    }

    // 当前只实现故事1,仅返回A的值,B暂时为0
    return fmt.Sprintf("%dA0B", a)
}

测试执行

创建 go.mod 文件:


gomod.png

运行测试:


tcode-succ.png

重构

重构代码:


refactor.png

重构后的代码:game.go

package game

import "fmt"

// Game 表示一个猜数字游戏
type Game struct {
    answer string // 游戏的答案(4位数字)
}

// NewGame 创建一个新的猜数字游戏
// answer: 表示游戏的答案,应该是4个不同的数字
func NewGame(answer string) *Game {
    return &Game{
        answer: answer,
    }
}

// Guess 猜测一个数字并返回结果
// number: 玩家猜测的数字
// 返回格式为"xAyB",其中x表示位置和数字都正确的个数,y表示仅数字正确但位置错误的个数
// 当前实现只关注故事1,只计算A值,B始终为0
func (g *Game) Guess(number string) string {
    countA := g.countPositionAndNumberMatch(number)
    return fmt.Sprintf("%dA0B", countA)
}

// countPositionAndNumberMatch 计算位置和数字都匹配的个数(A值)
func (g *Game) countPositionAndNumberMatch(number string) int {
    count := 0
    for i := 0; i < len(number) && i < len(g.answer); i++ {
        if number[i] == g.answer[i] {
            count++
        }
    }
    return count
}

再次运行测试:测试通过


test-ok.png

总结

AI 以 TDD 方式完成故事1开发后的自我总结:


summary.png

在 AI TDD 开发方式下,原来 TDD 繁琐的“红-绿-重构”实施步骤由 AI 自动完成,开发人员则专注于故事拆分,确保故事粒度与 AI 的现阶段能力高度匹配。从本文的实践可以看出,在编码阶段采用 AI TDD 开发方式能够大幅度提升编码的效率和质量,考虑后续在实际项目中试点和推广。

未来展望

当故事比较复杂时,还需要将故事进一步拆分成任务:

  • 故事关注业务价值,强调做正确的事,一般面向微服务的 API 进行测试,对应FT(Functional Test,功能测试);
  • 任务关注软件设计,强调正确的做事,一般面向微服务内部(分层分模块)的零部件进行测试,对应UT(Unit Test,单元测试)。

对于实际项目,在测试代码生成时还要考虑测试桩的提炼和测试关键字的封装。

在后续的文章中,我们再介绍相关的最佳实践,敬请期待!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容