TDD回顾
TDD(Test-Driven Development,测试驱动开发)是 XP(Extreme Programming,极限编程)实践中的一项核心技术,它以测试作为开发过程的中心,要求在编写任何产品代码之前,首先编写用于定义产品代码行为的测试,而编写的产品代码又要以使测试通过为目标。
TDD三个步骤:
- 添加一个测试,测试失败(变成红色);
- 快速使测试通过(变成绿色);
- 优化设计(变成蓝色)。
TDD三条军规:
- 不允许写任何产品代码,除非是为了让失败的测试用例能通过;
- 不允许写更多的产品代码,只要刚刚让失败的测试用例通过即可;
- 不允许写更多的测试代码,只要刚刚让测试失败即可,编译失败也算失败。
ATDD(Acceptance Test-Driven Development,验收测试驱动开发)中的测试是一种故事级别的 FT(Functional Test,功能测试),面向微服务的 API 进行测试。ATDD 确保做正确的事,关注业务价值,用 GWT 格式来表达故事的验收准则。
UTDD(Unit Test-Driven Development,单元测试驱动开发)中的测试是一种任务级别的UT,面向微服务内部的零部件进行测试。UTDD 确保正确的做事,关注软件设计,用 GWT 格式来表达任务的验收准则。
广义 TDD 既包含 UTDD 也包含 ATDD,而狭义 TDD 仅包含 UTDD,我们通常所说的 TDD 默认是狭义的。
ATDD 驱动做正确的事,关注用户价值,通过故事级别的“红-绿-重构”来实施。UTDD 驱动正确的做事,关注软件设计,通过任务级别的“红-绿-重构”来实施。通过 ATDD 和 UTDD 的紧密协作,确保以正确的方式做正确的事,从而高效地交付高质量的软件。
需求澄清
猜数字游戏的规则包括:
- 输入4个0~9中不同的数字,按enter键查阅结果是否正确(以“?A?B”形式显示)
说明: ?A表示所输入的?个数字和位置都与手机的答案相同; ?B表示有?个数字相同而位置有误。例如,输入“3609”时显示为“1A2B ”表示其中有一个数的数字和位置都对了;有两个数的数字对但位置不对;还有一个数的数字和位置都不对。 - 猜中数字,显示“4A0B”,游戏结束;
- 如果6次没猜中,那么游戏失败,显示“You are lose”。
TDD to do list拆分
故事级to do list拆分
故事 1: 作为玩家,我想要知道输入数中数字和位置都相同的个数,以便于赢得游戏
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_guess_both_ok | 假定答案是1234,且玩家输入5236 | 当玩家按下Enter 键 |
则手机显示 2A0B |
2 | test_repeat_digit_input | 假定玩家输入1231 | 当玩家按下Enter 键 |
则手机提示存在重复数字 |
3 | test_invalid_len_input | 假定玩家输入123 | 当玩家按下Enter 键 |
则手机提示长度应为4 |
4 | test_exist_non_digit_input | 假定玩家输入1a23 | 当玩家按下Enter 键 |
则手机提示包含非数字字符 |
故事 2: 作为玩家,我想要知道输入数中数字相同而位置有误的个数,以便于赢得游戏
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_guess_only_digit_ok | 假定答案是1234,且玩家输入4561 | 当玩家按下Enter 键 |
则手机显示 0A2B |
故事 3: 作为游戏设计者,我想要控制玩家尝试次数,以便于增加游戏的趣味性
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_guess_many_fail | 假定答案是1234,且玩家前五次的输入分别为 1567, 2478, 0324, 5678, 4321,然后第六次输入1432 | 当玩家第六次按下Enter 键 |
则手机显示 You are lose |
2 | test_guess_many_succ | 假定答案是1234,且玩家前五次的输入分别为 1567, 2478, 0324, 5678, 4321,然后第六次输入1234 | 当玩家按下Enter 键 |
则手机显示 4A0B |
故事 4: 作为游戏设计者,我想要记录玩家每一次的猜数结果,以便于查看猜数历史
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_guess_history | 假定答案是1234,且玩家前两次的输入分别是5236,4561,并且玩家第三次输入1234 | 当玩家查看猜数历史 | 则手机显示猜数历史记录有三条,分别是:5236 2A0B,4561 0A2B,1234 4A0B |
任务级to do list拆分
故事1 任务1:生成答案
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_generate_answer_by_once | 假定系统随机数是1964 | 当生成答案 | 那么答案为1964 |
2 | test_generate_answer_by_several_times | 假定系统两次生成的随机数分别是788, 2260 | 当生成答案 | 那么答案为7826 |
故事1 任务2:计算 ACount
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_a_count | 假定答案是1234,且玩家输入4231 | 当计算完成 | 那么 ACount 为 2 |
故事2 任务3:计算 BCount
编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
---|---|---|---|---|
1 | test_b_count | 假定答案是1234,且玩家输入4231 | 当计算完成 | 那么 BCount 为 2 |
Rust TDD编码实战
1.物理设计
使用 cargo new guess_number创建Rust代码工程,UT用例和产品代码放在src/lib.rs文件中,FT用例放在tests/guess_number_test.rs文件中,如下所示:
guess_number
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── guess_number_test.rs
在Cargo.toml文件中完成如下的配置:
[package]
name = "guess_number"
version = "0.1.0"
edition = "2021"
[lib]
name = "guess_number"
path = "src/lib.rs"
2.故事1编码
2.1任务1编码
任务1测试用例
任务1测试用例Prompt编写:
# TDD任务描述
基于系统的随机数生成器生成猜数游戏的答案,答案共有4位数字,每位数字各不相同,第一位数字不能为0。
# TDD任务验收准则
| 编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
| ---- | ------------------------------------- | --------------------------------------- | ---------------- | --------------- |
| 1 | test_generate_answer_by_once | 假定系统随机数是1964 | 当生成答案 | 那么答案为1964 |
| 2 | test_generate_answer_by_several_times | 假定系统两次生成的随机数分别是788, 2260 | 当生成答案 | 那么答案为7826 |
# 接口设计
fn generate_answer<R>(mut random_generator: R) -> u32
where
R: FnMut() -> u32,
{
...
}
# 输出要求
- 使用 Rust 语言编写UT用例;
- 使用givens, when, then三部分来描述测试用例;
- 在测试代码中对标准库的随机数生成函数进行打桩,以便满足验收准则的要求;
- 遵循接口设计要求,输出完整的测试用例代码,但不要生成**任何产品代码**。
# 任务描述
请你作为一名TDD专家级程序员,高质量完成测试用例。
使用大模型生成UT用例:
#[cfg(test)]
mod tests {
use super::*;
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Self { count: 0 }
}
fn next(&mut self) -> u32 {
self.count += 1;
match self.count {
1 => 788,
2 => 2260,
_ => 0, // 其他情况下返回占位符
}
}
}
#[test]
fn test_generate_answer_by_once() {
// Given: 假定系统随机数是1964
let random_generator = || 1964;
// When: 当生成答案
let answer = generate_answer(random_generator);
// Then: 那么答案为1964
assert_eq!(answer, 1964);
}
#[test]
fn test_generate_answer_by_several_times() {
// Given: 假定系统两次生成的随机数分别是788 和 2260
let mut counter = Counter::new();
let random_generator = |count: &mut Counter| count.next();
// When: 当生成答案
let answer = generate_answer(|| random_generator(&mut counter));
// Then: 那么答案为7826
assert_eq!(answer, 7826);
}
}
任务1产品代码
任务1产品代码Prompt编写:
# TDD任务描述
基于系统的随机数生成器生成猜数游戏的答案,要求答案的数字长度为4,且每一位各不相同。
# 测试用例
此处省略(拷贝之前大模型生成的测试用例)
# 输出要求
- 产品代码与测试代码分离;
- 产品代码的实现仅能依赖标准库函数,不得使用任何第三方库和Mock代码;
- 产品代码的实现必须通过测试用例。
# 任务描述
请你作为一名TDD专家级程序员,根据测试代码高质量完成产品代码,确保让测试刚刚通过即可,而不要过度实现。
使用大模型生成产品代码:
fn generate_answer<R>(mut random_generator: R) -> u32
where
R: FnMut() -> u32,
{
let mut digits = Vec::new();
let mut unique_digits = std::collections::HashSet::new();
while digits.len() < 4 {
let random_number = random_generator();
for digit_char in random_number.to_string().chars() {
if digits.len() < 4 {
let digit = digit_char.to_digit(10).unwrap();
// 确保第一位不为0
if digits.is_empty() && digit == 0 {
continue;
}
if unique_digits.insert(digit) {
digits.push(digit);
}
}
}
}
// Combine digits into a single number
digits.iter().fold(0u32, |acc, &d| acc * 10 + d)
}
运行测试:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --lib
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s
Running unittests src/lib.rs (target/debug/deps/guess_number-ec360b40bf376a5d)
running 2 tests
test tests::test_generate_answer_by_once ... ok
test tests::test_generate_answer_by_several_times ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
2.2任务2编码
任务2测试用例
任务2测试用例Prompt编写:
# TDD任务描述
ACount表示玩家输入的数字和位置都与答案相同的个数,请编写一个函数来计算ACount。
# TDD任务验收准则
| 编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
| ---- | ------------ | ------------------------------ | ---------------- | ---------------- |
| 1 | test_a_count | 假定答案是1234,且玩家输入4231 | 当计算完成 | 那么 ACount 为 2 |
---
# 接口设计
fn calc_acount(answer: u32, number: u32) -> u32
{
...
}
# 输出要求
- 使用 Rust 语言编写UT用例;
- 使用givens, when, then三部分来描述测试用例;
- 遵循接口设计要求,输出完整的测试用例代码,但不要生成**任何产品代码**。
# 任务描述
请你作为一名TDD专家级程序员,高质量完成测试用例。
使用大模型生成UT用例:
#[test]
fn test_a_count() {
// Given: 假定答案是1234,且玩家输入4231
let answer = 1234;
let number = 4231;
// When: 当计算完成
let acount = calc_acount(answer, number);
// Then: 那么 ACount 为 2
assert_eq!(acount, 2);
}
任务2产品代码
任务2产品代码Prompt编写:
# TDD任务描述
ACount表示玩家输入的数字和位置都与答案相同的个数,请编写一个函数来计算ACount。
# 测试用例
此处省略(拷贝之前大模型生成的测试用例)
# 输出要求
- 产品代码与测试代码分离;
- 产品代码的实现必须通过测试用例。
# 任务描述
请你作为一名TDD专家级程序员,根据测试代码高质量完成产品代码,确保让测试刚刚通过即可,而不要过度实现。
使用大模型生成产品代码:
fn calc_acount(answer: u32, number: u32) -> u32 {
let answer_str = format!("{:04}", answer);
let number_str = format!("{:04}", number);
let mut acount = 0;
for (a_char, n_char) in answer_str.chars().zip(number_str.chars()) {
if a_char == n_char {
acount += 1;
}
}
acount
}
运行测试:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --lib
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.16s
Running unittests src/lib.rs (target/debug/deps/guess_number-ec360b40bf376a5d)
running 3 tests
test tests::test_a_count ... ok
test tests::test_generate_answer_by_once ... ok
test tests::test_generate_answer_by_several_times ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
2.3故事1测试用例
故事1测试用例Prompt编写:
# TDD故事描述
作为玩家,我想要知道输入数中数字和位置都相同的个数,以便于赢得游戏。
# TDD故事验收准则
| 编号 | 用例名 | given (前置条件) | when (操作/事件) | then (预期结果) |
| ---- | -------------------------- | ------------------------------ | ------------------- | ------------------------ |
| 1 | test_guess_both_ok | 假定答案是1234,且玩家输入5236 | 当玩家按下`Enter`键 | 则手机显示 2A0B |
| 2 | test_repeat_digit_input | 假定玩家输入1231 | 当玩家按下`Enter`键 | 则手机提示存在重复数字 |
| 3 | test_invalid_len_input | 假定玩家输入123 | 当玩家按下`Enter`键 | 则手机提示长度应为4 |
| 4 | test_exist_non_digit_input | 假定玩家输入1a23 | 当玩家按下`Enter`键 | 则手机提示包含非数字字符 |
---
# 接口设计
fn generate_answer<R>(mut random_generator: R) -> u32
where
R: FnMut() -> u32,
{
...
}
设计一个类Game:
- 在Game的构造函数new函数,输入参数为一个随机数生成器random_generator,是generate_answer函数的泛型参数,这个参数有一个默认的值,是系统的默认随机数生成器,测试用例可以覆盖这个默认值;
- 提供方法guess,输入参数为number,是游戏玩家输入的数字;
- 返回值为result,对应验收准则中的预期结果(then)。
# 输出要求
- 使用 Rust 语言编写FT用例(集成测试);
- 使用givens, when, then三部分来描述测试用例;
- FT用例放在tests/guess_number.rs文件中;
- 遵循接口设计要求,输出完整的测试用例代码,但不要生成**任何产品代码**。
# 任务描述
请你作为一名TDD专家级程序员,高质量完成测试用例。
使用大模型生成FT用例:
// tests/guess_number.rs
use guess_number::Game;
#[test]
fn test_guess_both_ok() {
// Given: 假定答案是1234,且玩家输入5236
// 模拟随机数生成器,使答案为1234
let mut call_count = 0;
let random_generator = || {
call_count += 1;
match call_count {
1 => 1234, // 第一次调用返回1234,作为答案
_ => 0, // 其他调用返回0(不会影响结果)
}
};
// 创建游戏实例,传入模拟的随机数生成器
let game = Game::new(random_generator);
let number = "5236";
// When: 当玩家按下`Enter`键
let result = game.guess(number);
// Then: 则手机显示 2A0B
assert_eq!(result, "2A0B");
}
#[test]
fn test_repeat_digit_input() {
// Given: 假定玩家输入1231
// 使用返回非零数字的随机数生成器
let random_generator = || 1234; // 返回一个有效的四位数
let game = Game::new(random_generator);
let number = "1231";
// When: 当玩家按下`Enter`键
let result = game.guess(number);
// Then: 则手机提示存在重复数字
assert_eq!(result, "存在重复数字");
}
#[test]
fn test_invalid_len_input() {
// Given: 假定玩家输入123
let random_generator = || 1234; // 返回一个有效的四位数
let game = Game::new(random_generator);
let number = "123";
// When: 当玩家按下`Enter`键
let result = game.guess(number);
// Then: 则手机提示长度应为4
assert_eq!(result, "长度应为4");
}
#[test]
fn test_exist_non_digit_input() {
// Given: 假定玩家输入1a23
let random_generator = || 1234; // 返回一个有效的四位数
let game = Game::new(random_generator);
let number = "1a23";
// When: 当玩家按下`Enter`键
let result = game.guess(number);
// Then: 则手机提示包含非数字字符
assert_eq!(result, "包含非数字字符");
}
2.4故事1产品代码
故事1产品代码Prompt编写:
# TDD故事描述
作为玩家,我想要知道输入数中数字和位置都相同的个数,以便于赢得游戏。
# 测试用例
此处省略(拷贝之前大模型生成的测试用例)
# 依赖的API
fn generate_answer<R>(mut random_generator: R) -> u32
where
R: FnMut() -> u32,
{
...
}
fn calc_acount(answer: u32, number: u32) -> u32
{
...
}
# 输出要求
- 产品代码与测试代码分离;
- 产品代码的实现必须通过测试用例。
# 任务描述
请你作为一名TDD专家级程序员,根据测试代码高质量完成产品代码,确保让测试刚刚通过即可,而不要过度实现。
使用大模型生成故事1的产品代码:
// src/lib.rs
pub struct Game {
answer: u32,
}
impl Game {
pub fn new<R>(mut random_generator: R) -> Self
where
R: FnMut() -> u32,
{
let answer = generate_answer(&mut random_generator);
Game { answer }
}
pub fn guess(&self, number: &str) -> String {
// 检查输入长度是否为4
if number.len() != 4 {
return "长度应为4".to_string();
}
// 检查输入是否全部为数字
if !number.chars().all(|c| c.is_ascii_digit()) {
return "包含非数字字符".to_string();
}
// 检查是否存在重复数字
let mut digits_set = std::collections::HashSet::new();
if !number.chars().all(|c| digits_set.insert(c)) {
return "存在重复数字".to_string();
}
// 将输入转换为 u32
let number_u32 = number.parse::<u32>().unwrap();
// 计算 ACount
let a_count = calc_acount(self.answer, number_u32);
// 返回结果,暂时不计算 BCount,满足测试即可
format!("{}A0B", a_count)
}
}
运行故事级测试用例:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --test guess_number_test
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.37s
Running tests/guess_number_test.rs (target/debug/deps/guess_number_test-eb39a5b5a904e41b)
running 4 tests
test test_guess_both_ok ... ok
test test_exist_non_digit_input ... ok
test test_invalid_len_input ... ok
test test_repeat_digit_input ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
3.故事2编码
3.1任务3编码
任务3测试用例
参考前面的任务编码编写任务3的Prompt,此处省略。
使用大模型生成UT用例:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_b_count() {
// Given: 假定答案是1234,且玩家输入4231
let answer = 1234;
let number = 4231;
// When: 当计算完成
let bcount = calc_bcount(answer, number);
// Then: 那么 BCount 为 2
assert_eq!(bcount, 2);
}
}
使用大模型生成产品代码:
fn calc_bcount(answer: u32, number: u32) -> u32 {
let answer_str = format!("{:04}", answer);
let number_str = format!("{:04}", number);
// 计算 ACount
let mut acount = 0;
for (a_char, n_char) in answer_str.chars().zip(number_str.chars()) {
if a_char == n_char {
acount += 1;
}
}
// 计算共同的数字数量
let answer_digits: std::collections::HashSet<char> = answer_str.chars().collect();
let number_digits: std::collections::HashSet<char> = number_str.chars().collect();
let common_digits = answer_digits.intersection(&number_digits).count() as u32;
// 计算 BCount
let bcount = common_digits - acount;
bcount
}
运行UT测试:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --lib
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guess_number-ec360b40bf376a5d)
running 4 tests
test tests::test_a_count ... ok
test tests::test_generate_answer_by_once ... ok
test tests::test_b_count ... ok
test tests::test_generate_answer_by_several_times ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
3.2故事2测试用例
参考故事1测试用例的Prompt编写故事2的,然后使用大模型生成FT用例:
// tests/guess_number.rs
use guess_number::Game;
#[test]
fn test_guess_only_digit_ok() {
// Given: 假定答案是1234,且玩家输入4561
// 模拟随机数生成器,使答案为1234
let mut call_count = 0;
let random_generator = || {
call_count += 1;
match call_count {
1 => 1234, // 第一次调用返回1234,作为答案
_ => 0, // 其他调用返回0(不会影响结果)
}
};
// 创建游戏实例,传入模拟的随机数生成器
let game = Game::new(random_generator);
let number = "4561";
// When: 当玩家按下`Enter`键
let result = game.guess(number);
// Then: 则手机显示 0A2B
assert_eq!(result, "0A2B");
}
3.2故事2产品代码
参考故事1产品代码的Prompt编写故事2的,然后使用大模型生成产品代码(仅最后几行有修改):
pub fn guess(&self, number: &str) -> String {
// 检查输入长度是否为4
if number.len() != 4 {
return "长度应为4".to_string();
}
// 检查输入是否全部为数字
if !number.chars().all(|c| c.is_ascii_digit()) {
return "包含非数字字符".to_string();
}
// 检查是否存在重复数字
let mut digits_set = std::collections::HashSet::new();
if !number.chars().all(|c| digits_set.insert(c)) {
return "存在重复数字".to_string();
}
// 将输入转换为 u32
let number_u32 = number.parse::<u32>().unwrap();
// Calculate ACount and BCount
let a_count = calc_acount(self.answer, number_u32);
let b_count = calc_bcount(self.answer, number_u32);
// Format the result
format!("{}A{}B", a_count, b_count)
}
运行FT用例:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --test guess_number_test
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.98s
Running tests/guess_number_test.rs (target/debug/deps/guess_number_test-eb39a5b5a904e41b)
running 5 tests
test test_exist_non_digit_input ... ok
test test_invalid_len_input ... ok
test test_guess_both_ok ... ok
test test_repeat_digit_input ... ok
test test_guess_only_digit_ok ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
4.故事3编码
4.1故事3测试用例
参考故事2测试用例的Prompt编写故事3的,然后使用大模型生成FT用例:
#[test]
fn test_guess_many_fail() {
// Given: 假定答案是1234,且玩家前五次的输入分别为 1567, 2478, 0324, 5678, 4321,然后第六次输入1432
// 模拟随机数生成器,使答案为1234
let mut call_count = 0;
let random_generator = || {
call_count += 1;
match call_count {
1 => 1234, // 第一次调用返回1234,作为答案
_ => 0, // 其他调用返回0(不会影响结果)
}
};
// 创建游戏实例,传入模拟的随机数生成器
let mut game = Game::new(random_generator);
// Define the sequence of guesses
let guesses = ["1567", "2478", "0324", "5678", "4321", "1432"];
// When: 当玩家前五次按下`Enter`键
for guess in &guesses[..5] {
let _ = game.guess(guess);
}
// 当玩家第六次按下`Enter`键
let final_result = game.guess(guesses[5]);
// Then: 则手机显示 You are lose
assert_eq!(final_result, "You are lose");
}
#[test]
fn test_guess_many_succ() {
// Given: 假定答案是1234,且玩家前五次的输入分别为 1567, 2478, 0324, 5678, 4321,然后第六次输入1234
// 模拟随机数生成器,使答案为1234
let mut call_count = 0;
let random_generator = || {
call_count += 1;
match call_count {
1 => 1234, // 第一次调用返回1234,作为答案
_ => 0, // 其他调用返回0(不会影响结果)
}
};
// 创建游戏实例,传入模拟的随机数生成器
let mut game = Game::new(random_generator);
// Define the sequence of guesses
let guesses = ["1567", "2478", "0324", "5678", "4321", "1234"];
// When: 当玩家前五次按下`Enter`键
for guess in &guesses[..5] {
let _ = game.guess(guess);
}
// 当玩家第六次按下`Enter`键
let final_result = game.guess(guesses[5]);
// Then: 则手机显示 4A0B
assert_eq!(final_result, "4A0B");
}
4.2故事3产品代码
参考故事2产品代码的Prompt编写故事3的,然后使用大模型生成产品代码:
// The `Game` struct represents the state of a number guessing game.
///
/// It holds the answer that players need to guess and the number of attempts made.
pub struct Game {
answer: u32,
attempts: u32,
}
impl Game {
/// Creates a new instance of the game with a generated answer.
///
/// # Parameters
/// - `random_generator`: A closure that generates random numbers.
///
/// # Returns
/// - A new `Game` instance with a unique 4-digit answer.
pub fn new<R>(random_generator: R) -> Self
where
R: FnMut() -> u32,
{
let answer = generate_answer(random_generator);
Game {
answer,
attempts: 0,
}
}
/// Processes the player's guess and returns the result.
///
/// # Parameters
/// - `number`: The player's guess as a string slice.
///
/// # Returns
/// - A `String` representing the result.
/// - If the player exceeds the maximum attempts, returns "You are lose".
/// - If the guess is correct, returns "4A0B".
/// - Otherwise, returns "XA YB", where X is ACount and Y is BCount.
pub fn guess(&mut self, number: &str) -> String {
// Check if the input length is exactly 4
if number.len() != 4 {
return "长度应为4".to_string();
}
// Check if all characters are digits
if !number.chars().all(|c| c.is_ascii_digit()) {
return "包含非数字字符".to_string();
}
// Check for duplicate digits
let mut seen = HashSet::new();
if !number.chars().all(|c| seen.insert(c)) {
return "存在重复数字".to_string();
}
// Parse the input number
let number_u32 = match number.parse::<u32>() {
Ok(num) => num,
Err(_) => return "包含非数字字符".to_string(),
};
// Increment attempts
self.attempts += 1;
// Calculate ACount and BCount
let a_count = calc_acount(self.answer, number_u32);
let b_count = calc_bcount(self.answer, number_u32);
// Check for win condition
if a_count == 4 {
return "4A0B".to_string();
}
// Check if maximum attempts have been reached
if self.attempts >= 6 {
return "You are lose".to_string();
}
// Format the result
format!("{}A{}B", a_count, b_count)
}
}
运行FT用例:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --test guess_number_test
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.40s
Running tests/guess_number_test.rs (target/debug/deps/guess_number_test-eb39a5b5a904e41b)
running 6 tests
test test_exist_non_digit_input ... ok
test test_guess_both_ok ... ok
test test_guess_many_fail ... ok
test test_guess_many_succ ... ok
test test_invalid_len_input ... ok
test test_repeat_digit_input ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
5.故事4编码
5.1故事4测试用例
参考故事3测试用例的Prompt编写故事4的,然后使用大模型生成FT用例:
#[test]
fn test_guess_history() {
// Given: The answer is 1234, and the player's first two inputs are "5236" and "4561"
// Player's third input is "1234"
// Simulate the random number generator to return 1234 as the answer
let mut call_count = 0;
let random_generator = || {
call_count += 1;
match call_count {
1 => 1234, // First call returns 1234 as the answer
_ => 0, // Subsequent calls return 0 (placeholder)
}
};
// Create a new game instance with the simulated random generator
let mut game = Game::new(random_generator);
// Define the sequence of guesses
let guesses = ["5236", "4561", "1234"];
// When: The player makes the first two guesses
for guess in &guesses[..2] {
let _ = game.guess(guess);
}
// When: The player makes the third guess
let _ = game.guess(guesses[2]);
// When: The player views the guess history
// Assume that the Game struct has a method `get_history` that returns a Vec<String>
let history = game.get_history();
// Then: The mobile displays the guess history with three entries
let expected_history = vec!["5236 2A0B", "4561 0A2B", "1234 4A0B"];
assert_eq!(history, expected_history);
}
5.2故事4产品代码
参考故事3产品代码的Prompt编写故事4的,然后使用大模型生成产品代码:
/// The `Game` struct represents the state of a number guessing game.
///
/// It holds the answer that players need to guess, the number of attempts made,
/// and a history of all guesses along with their results.
pub struct Game {
answer: u32,
attempts: u32,
history: Vec<String>, // 新增字段,用于记录猜数历史
}
impl Game {
/// Creates a new instance of the game with a generated answer.
///
/// # Parameters
/// - `random_generator`: A closure that generates random numbers.
///
/// # Returns
/// - A new `Game` instance with a unique 4-digit answer.
pub fn new<R>(random_generator: R) -> Self
where
R: FnMut() -> u32,
{
let answer = generate_answer(random_generator);
Game {
answer,
attempts: 0,
history: Vec::new(), // 初始化猜数历史为空
}
}
/// Processes the player's guess and returns the result.
///
/// # Parameters
/// - `number`: The player's guess as a string slice.
///
/// # Returns
/// - A `String` representing the result.
/// - If the player exceeds the maximum attempts, returns "You are lose,游戏失败".
/// - If the guess is correct, returns "4A0B,游戏结束".
/// - Otherwise, returns "XA YB", where X is ACount and Y is BCount.
pub fn guess(&mut self, number: &str) -> String {
// Check if the input length is exactly 4
if number.len() != 4 {
return "长度应为4".to_string();
}
// Check if all characters are digits
if !number.chars().all(|c| c.is_ascii_digit()) {
return "包含非数字字符".to_string();
}
// Check for duplicate digits
let mut seen = HashSet::new();
if !number.chars().all(|c| seen.insert(c)) {
return "存在重复数字".to_string();
}
// Parse the input number
let number_u32 = match number.parse::<u32>() {
Ok(num) => num,
Err(_) => return "包含非数字字符".to_string(),
};
// Increment attempts
self.attempts += 1;
// Calculate ACount and BCount
let a_count = calc_acount(self.answer, number_u32);
let b_count = calc_bcount(self.answer, number_u32);
// Determine the result based on ACount and BCount
let result = if a_count == 4 {
"4A0B".to_string()
} else if self.attempts >= 6 {
"You are lose".to_string()
} else {
format!("{}A{}B", a_count, b_count)
};
// Record the guess and result to history
self.history.push(format!("{} {}", number, result));
result
}
/// Retrieves the history of guesses.
///
/// # Returns
/// - A `Vec<String>` containing each guess and its corresponding result.
pub fn get_history(&self) -> Vec<String> {
self.history.clone()
}
}
运行FT用例:
zhangxiaolongdeMacBook-Pro:guess_number zhangxiaolong$ cargo test --test guess_number_test
Compiling guess_number v0.1.0 (/Users/zhangxiaolong/Desktop/D/rust-workspace/guess_number)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running tests/guess_number_test.rs (target/debug/deps/guess_number_test-eb39a5b5a904e41b)
running 7 tests
test test_exist_non_digit_input ... ok
test test_guess_both_ok ... ok
test test_guess_history ... ok
test test_guess_many_fail ... ok
test test_invalid_len_input ... ok
test test_guess_many_succ ... ok
test test_repeat_digit_input ... ok
6.main函数
6.1main函数编码
我们将main函数简单设计一下:
- 通过终端与玩家进行交互,提示玩家输入猜测,并显示每次猜测的结果;
- 定义了默认的随机数生成器,并注入给game对象。
- 允许玩家最多进行6次猜测,若在此期间猜中答案则提前结束游戏,否则在6次尝试后显示猜数历史。
使用大模型生成main函数的代码:
/ src/main.rs
use guess_number::Game;
use std::io::{self, Write};
fn main() {
// Given: The answer is generated using a default random number generator
let mut call_count = 0;
let random_generator = || {
call_count += 1;
// Generate a random u32 number between 1000 and 9999 to ensure it's 4-digit
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(1000..=9999)
};
// Create a new game instance with the default random generator
let mut game = Game::new(random_generator);
// Variable to store the last guess result
let mut last_result = String::new();
// When: The player makes up to 6 guesses
for _ in 1..=6 {
// Prompt the player to enter a guess
print!("请输入你的猜测 (4个不同的数字0-9): ");
io::stdout().flush().unwrap(); // Ensure the prompt is displayed
// Read the player's input
let mut input = String::new();
if let Err(e) = io::stdin().read_line(&mut input) {
eprintln!("读取输入失败: {}", e);
continue; // Prompt again if there's an input error
}
let guess = input.trim();
// When: The player makes a guess
let result = game.guess(guess);
// Then: Display the result of the guess
println!("{}", result);
// Store the last result
last_result = result.clone();
// If the guess is correct (4A0B), break the loop and end the game
if result == "4A0B" {
break;
}
}
if last_result.starts_with("You are lose") {
println!("\n猜数历史记录:");
for entry in game.get_history() {
println!("{}", entry);
}
}
}
guess_number工程使用了 rand crate 来生成随机数,需要在Cargo.toml文件中添加 rand 依赖:
[dependencies]
rand = "0.8" # 使用最新版本,如有必要
6.2main函数测试
(1)非法输入 & 多次猜测成功
(2)一次猜测成功
(3)6次猜测失败
总结
本文通过猜数游戏的编码实战案例,展示了“LLM+TDD”组合的威力:即使你对Rust语言不熟悉,但是在“LLM+TDD”的加持下,你仍然可能低成本完成Rust编码任务。
确定性Prompt TDD
我们通常将“LLM+TDD”组合的技术称之为确定性Prompt TDD:
说明如下:
- 大模型根据 Prompt 直接生成产品代码,阅读理解和调测代码会占用较大精力,平均占用大模型编码时间的 50% 以上;
- 大模型根据 Prompt 生成的用例比较单一,且易于理解,开发人员可以快速发现问题并进行修正;
- 用例表达的是需求,用例校验产品代码通过则代表产品代码实现了需求。
LLM辅助TDD开发的流程
当人工完成TDD开发时,需要关注:
- 先考虑ATDD循环(故事级to do list拆分 -> 写FT -> 驱动业务流程代码的开发和重构);
- 当FT比较复杂时,再考虑UTDD循环(任务级to do list拆分 -> 写UT -> 驱动业务逻辑代码的开发和重构)。
当LLM辅助完成TDD开发时,需要关注:
- 故事级to do list拆分和任务级to do list拆分还是由人工来完成,但验收准则则可以先由LLM辅助生成,然后人工再进行确认和修正(本文为了控制自身的规模,故事级和任务级的验收准则都由人工直接来完成,同时在TDD编码实战中仅实施了“红-绿”过程,而跳过了“重构”步骤);
- 从验收准则到用例再到代码,每一步都需要进行确认,大模型直接生成全对的情况比较少,所以图中LLM辅助完成TDD时,大多情况都强调了人工修正。
实践中的几点经验
在LLM辅助TDD编码实践中,我们总结了几点经验:
- 任务编号规则:在新需求中,任务与故事一样,都是全局编号,零个或多个任务归属于一个故事;
- 任务完成优先:在编写故事的 FT 用例前,先将该故事依赖的所有任务逐一开发完成;
- 代码提交的粒度:故事粒度较大,尽量按照任务粒度来进行提交;
- 大模型生成用例的策略:大模型生成 FT(或 UT)时,可以按照故事(或任务)的粒度批量生成用例,但要注意的是,提示词中的示例必须与当前场景高度匹配。