LLM辅助Rust TDD编码实战

TDD回顾

TDD(Test-Driven Development,测试驱动开发)是 XP(Extreme Programming,极限编程)实践中的一项核心技术,它以测试作为开发过程的中心,要求在编写任何产品代码之前,首先编写用于定义产品代码行为的测试,而编写的产品代码又要以使测试通过为目标。


TDD.png

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.png

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)非法输入 & 多次猜测成功


many_succ.png

(2)一次猜测成功


once_succ.png

(3)6次猜测失败


many_fail.png

总结

本文通过猜数游戏的编码实战案例,展示了“LLM+TDD”组合的威力:即使你对Rust语言不熟悉,但是在“LLM+TDD”的加持下,你仍然可能低成本完成Rust编码任务。

确定性Prompt TDD

我们通常将“LLM+TDD”组合的技术称之为确定性Prompt TDD:


prompt-tdd.png

说明如下:

  • 大模型根据 Prompt 直接生成产品代码,阅读理解和调测代码会占用较大精力,平均占用大模型编码时间的 50% 以上;
  • 大模型根据 Prompt 生成的用例比较单一,且易于理解,开发人员可以快速发现问题并进行修正;
  • 用例表达的是需求,用例校验产品代码通过则代表产品代码实现了需求。

LLM辅助TDD开发的流程

workflow.png

当人工完成TDD开发时,需要关注:

  • 先考虑ATDD循环(故事级to do list拆分 -> 写FT -> 驱动业务流程代码的开发和重构);
  • 当FT比较复杂时,再考虑UTDD循环(任务级to do list拆分 -> 写UT -> 驱动业务逻辑代码的开发和重构)。

当LLM辅助完成TDD开发时,需要关注:

  • 故事级to do list拆分和任务级to do list拆分还是由人工来完成,但验收准则则可以先由LLM辅助生成,然后人工再进行确认和修正(本文为了控制自身的规模,故事级和任务级的验收准则都由人工直接来完成,同时在TDD编码实战中仅实施了“红-绿”过程,而跳过了“重构”步骤);
  • 从验收准则到用例再到代码,每一步都需要进行确认,大模型直接生成全对的情况比较少,所以图中LLM辅助完成TDD时,大多情况都强调了人工修正。

实践中的几点经验

experience.png

在LLM辅助TDD编码实践中,我们总结了几点经验:

  • 任务编号规则:在新需求中,任务与故事一样,都是全局编号,零个或多个任务归属于一个故事;
  • 任务完成优先:在编写故事的 FT 用例前,先将该故事依赖的所有任务逐一开发完成;
  • 代码提交的粒度:故事粒度较大,尽量按照任务粒度来进行提交;
  • 大模型生成用例的策略:大模型生成 FT(或 UT)时,可以按照故事(或任务)的粒度批量生成用例,但要注意的是,提示词中的示例必须与当前场景高度匹配。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容