TDD TDD 和重构练习-FizzBuzz Sprint 1

admin · December 31, 2020 · 41 hits
Topic has been selected as the excellent topic by the admin.

FizzBuzz

For

Kobe Bryant details how Kevin Durant can get even better | 'Detail' Excerpt | ESPN

Sprint 1

  • Product backlog
    1. 从 1 至 100 依次报数,如第 1 位报 “1”,第 2 位报 “2”
    2. 如果碰到被 3 整除的数则报 “Fizz”
    3. 如果碰到被 5 整除的数则报 “Buzz”
    4. 如果同时被 3 和 5 整除则报 “FizzBuzz”

操作分解

下面以 3 分钟为时限来组织操作,每一组完成一个明确的目标。

第 1 组操作:创建工程和配置

  • 创建工程

    • 开发环境:IntelliJ IDEA Community Edition 2019.1.1 x64
    • 编程语言:Java8
    • 构建系统:Gradle
  • 配置单元测试

    • Junit5

Junit5 不知道如何配置?

步骤 动作 结果 用时
1 通过搜索引擎搜索 Junit5 找到官方主页https://junit.org/junit5/ 10s
2 浏览主页,寻找文档链接 找到User Guide 10s
3 浏览文档目录,寻找Gradle的配置 找到4.2.1节:Running Test -> Build Support -> Gradle 10.1.2节:JUnit Jupiter 30s
4 照文档修改build.gradle 修改结果如下 60s
test {
    useJUnitPlatform()
}

dependencies {
    testCompile("org.junit.jupiter:junit-jupiter-api:5.4.2")
    testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")
    testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.2")
}
  • Refresh Gradle project

一般会自动刷新,也可手动刷新:在BuildGradle窗口的左上角都有该刷新按钮。

第 2 组操作:Hello, Junit

创建测试类/文件

步骤 动作 结果 用时
1 Alt+1 跳到 Project 窗口 1s
2 ↓→↑← 定位到src/test/java目录 3s
3 Alt+Ins 弹出 Generate 或 New 菜单 1s
4 选择Java Class回车 弹出Create New Class对话框 1s
5 输入类名FizzBuzzTest回车 跳到新建的类编辑窗口 5s

验证单元测试可工作

步骤 动作 结果 用时
1 Alt+Ins 弹出 Generate 菜单 1s
2 选择Test Method回车 新建一个测试方法,并选择方法名 1s
3 输入方法名should_work_ok回车 进入函数体 3s
4 开始写第一行代码 输入代码如下 10s
5 Ctrl+Shift+F10 运行该测试方法 Run窗口显示Test Result 5s
assertEquals(1, 1);

第 3 组操作:实现第 1 条 PBI

为该 PBI 创建测试方法

借助 IDE 的代码生成功能,快速完成类和函数的创建。

步骤 动作 结果 用时
1 Alt+Ins 弹出 Generate 菜单 1s
2 选择Test Method回车 新建一个测试方法,并选择方法名 1s
3 输入方法名should_input_1_return_1回车 进入函数体 3s
4 开始写第一行代码 输入代码如下 10s
5 光标定位在 FizzBuzz,按 Alt+Enter 弹出 Generate 对话框 3s
6 选择Create Class FizzBuzz回车 弹出对话框配置包名和路径 1s
7 包名留空,路径选择 main,回车 进入新建的类FizzBuzz编辑区 3s
8 Ctrl+Tab 切换回测试类编辑区 1s
9 光标定位到 FizzBuzz 构造函数,按 Alt+Enter 弹出 Generate 对话框 3s
10 选择Create constructor回车 跳到类FizzBuzz新建的构造函数 1s
11 Shift+F10 运行上次运行过的测试方法 Run窗口显示Test Result 5s
12 结果测试失败,红色提示
@Test
void should_input_1_return_1() {
    FizzBuzz item = new FizzBuzz(1);
    assertEquals("1", item.toString());
}

快速让该测试通过

步骤 动作 结果 用时
1 在 FizzBuzz 类编辑区,按 Ctrl+O 弹出选择 Override 方法菜单 1s
2 选择toString方法,回车 跳到 toString 函数体内 3s
3 修改代码,让测试通过 代码结果如下 10s
4 Shift+F10 运行上次运行过的测试方法 Run窗口显示Test Result 5s
5 结果测试成功,绿色提示
@Override
public String toString() {
    return "1";
}

重构代码

步骤 动作 结果 用时
1 修改 toString 实现 代码结果如下toString部分 10s
2 光标定位在 value,按 Alt+Enter 弹出 Generate 对话框 3s
3 选择Create Field,回车 跳到新建的成员变量处 1s
4 在构造函数内为该成员变量赋值 代码结果如下构造函数部分 10s
4 Shift+F10 运行测试 仍是绿色,说明重构没出错 5s
@Override
public String toString() {
    return String.valueOf(this.value);
}

public FizzBuzz(int i) {
    this.value = i;
}

第 4 组操作:实现第 2 条 PBI

为 PBI2 创建测试方法

步骤 动作 结果 用时
1 Ctrl+Tab 切回到测试代码编辑区 1s
2 选中已有的一个测试方法,按 Ctrl+D 复制一份新的测试方法 3s
3 为新复制的方法改名 代码如下:仅改函数名 3s
4 修改新测试代码以实现 PBI2 代码如下:函数内容 10s
5 光标定位到测试方法外,测试类里面,Ctrl+Shift+F10 运行测试 这样可以测试全部方法,结果是红色 5s
@Test
void should_input_3_return_Fizz() {
    FizzBuzz item = new FizzBuzz(3);
    assertEquals("Fizz", item.toString());
}

快速让测试should_input_3_return_Fizz通过

从此处开始省略Baby Steps描述。

修改toString实现,然后运行测试,绿色。

@Override
public String toString() {
    if (this.value % 3 == 0) return "Fizz";
    return String.valueOf(this.value);
}

重构测试代码

使用 Junit5 的参数化测试方法,可以消除重复的测试方法,然后运行测试,绿色。

@ParameterizedTest(name = "should return {1} given {0}")
@CsvSource({
        "1, 1",
        "3, Fizz",
})
void should_test_sprint_1(int input, String expected) {
    FizzBuzz item = new FizzBuzz(input);
    assertEquals(expected, item.toString());
}

第 5 组操作:实现第 3 条 PBI

  • 为该 PBI 添加测试数据,然后运行测试,红色

    测试方法的CsvSource内增加一条数据:"5, Buzz"

  • 快速让测试通过

    FizzBuzztoString方法内增加新逻辑,代码如下,然后运行测试,绿色

@Override
public String toString() {
    if (this.value % 3 == 0) return "Fizz";
    if (this.value % 5 == 0) return "Buzz";
    return String.valueOf(this.value);
}
  • 重构:提取函数

toString内有 2 个if表达式重复了,可以提取出到 1 个函数里面,代码如下。

从此处开始省略运行测试的提示,每次代码改动结束后,都应该运行测试。

@Override
public String toString() {
    if (isDivBy(3)) return "Fizz";
    if (isDivBy(5)) return "Buzz";
    return String.valueOf(this.value);
}

private boolean isDivBy(int i) {
    return this.value % i == 0;
}

第 6 组操作:实现第 4 条 PBI

  • 为该 PBI 添加测试数据

    测试方法的CsvSource内增加一条数据:"15, FizzBuzz"

  • 快速让测试通过

    FizzBuzztoString方法内增加新逻辑
    将光标移到到isDivBy(5)这一行,Ctrl+C选中该行,Ctrl+D复制选中内容到其之后,然后修改以适合 PBI4。

@Override
public String toString() {
    if (isDivBy(3)) return "Fizz";
    if (isDivBy(5)) return "Buzz";
    if (isDivBy(15)) return "FizzBuzz";
    return String.valueOf(this.value);
}
  • 修复错误

    上述修改没有一次性使测试通过,输入 15 时,期望得到FizzBuzz,结果却是Fizz,说明出现了 Bug。
    经查代码,可以快速发现该错误是第一条条件语句if (isDivBy(3)) return "Fizz";造成,要想正确处理,需要调整这几条条件语句的顺序,修改如下:

@Override
public String toString() {
    if (isDivBy(15)) return "FizzBuzz";
    if (isDivBy(3)) return "Fizz";
    if (isDivBy(5)) return "Buzz";
    return String.valueOf(this.value);
}
  • 重构:消灭代码坏味道

    toString方法里已经包含了 4 个条件判断的逻辑,可以预见后续迭代中,修改都会集中到该方法中,形成Long Method过长函数。
    该重构过程需要步骤较多,故单独形成一组操作。

第 7 组操作:重构过长函数

  • 观察到 4 个条件语句具有相同的结构
    输入一个数字,输出一个字符串,只是判断的表达式不一样
  • 明确该方法的职责
    该方法最终要返回一个字符串,说明这是其本职,判断逻辑可以委托出去
  • 第一条判断逻辑看起来像是后俩条逻辑的组合,有点复杂,所以先动第二条逻辑,尝试委托出去
@Override
public String toString() {
    if (isDivBy(15)) return "FizzBuzz";

    String result1 = ruleFizzResult();
    if (!result1.isEmpty()) return result1;

    if (isDivBy(5)) return "Buzz";

    return String.valueOf(this.value);
}

private String ruleFizzResult() {
    if (isDivBy(3)) return "Fizz";
    return "";
}
  • 尝试委托第三条逻辑
@Override
public String toString() {
    if (isDivBy(15)) return "FizzBuzz";

    String result1 = ruleFizzResult();
    if (!result1.isEmpty()) return result1;

    String result2 = ruleBuzzResult();
    if (!result2.isEmpty()) return result2;

    return String.valueOf(this.value);
}

private String ruleFizzResult() {
    if (isDivBy(3)) return "Fizz";
    return "";
}

private String ruleBuzzResult() {
    if (isDivBy(5)) return "Buzz";
    return "";
}
  • 尝试组合实现第一条逻辑
@Override
public String toString() {
    String result1 = ruleFizzResult();
    String result2 = ruleBuzzResult();

    if (!result1.isEmpty() && !result2.isEmpty()) {
        return result1 + result2;
    }

    if (!result1.isEmpty()) return result1;

    if (!result2.isEmpty()) return result2;

    return String.valueOf(this.value);
}
  • 消除重复的对resultx的判断
@Override
public String toString() {
    String result1 = ruleFizzResult();
    String result2 = ruleBuzzResult();

    String result = result1 + result2;

    if (!result.isEmpty()) return result;

    return String.valueOf(this.value);
}
  • 消除对 rule 规则结果的重复获取
@Override
public String toString() {

    String[] results = getAllRuleResult();

    String result = String.join("", results);

    if (!result.isEmpty()) return result;

    return String.valueOf(this.value);
}

private String[] getAllRuleResult() {
    String result1 = ruleFizzResult();
    String result2 = ruleBuzzResult();

    return new String[] {result1, result2};
}
  • 继续理清toString的职责
    首先获取了所有原子规则的结果,
    接着应用组合规则,生成组合的结果,该结果也兼容了原子规则
    最后是默认规则,但所有明确的规则都失效后才使用。
    对结果的处理可以提取函数为一个独立职责。
@Override
public String toString() {
    String[] results = getAtomicRuleResult();
    return getComponentRuleResult(results);
}

private String getComponentRuleResult(String[] results) {
    String result = String.join("", results);

    if (!result.isEmpty()) return result;

    return String.valueOf(this.value);
}

// `重命名函数`为原则规则的结果
private String[] getAtomicRuleResult() {
    String result1 = ruleFizzResult();
    String result2 = ruleBuzzResult();

    return new String[] {result1, result2};
}
  • 优化组合规则结果的操作逻辑

    使用Collection Pipelines,代替使用原始的逻辑判断if

private String getComponentRuleResult(String[] results) {
    return Arrays.stream(results)
        .filter(v -> !v.isEmpty())
        .reduce(String::concat)
        .orElse(String.valueOf(this.value));
}
  • 优化原子规则获取结果操作

    使用Inline Temp将临时变量内联化。

private String[] getAtomicRuleResult() {
    return new String[] {ruleFizzResult(), ruleBuzzResult()};
}
  • 截止目前的重构结果
@Override
public String toString() {
    String[] results = getAtomicRuleResult();
    return getComponentRuleResult(results);
}

private String[] getAtomicRuleResult() {
    return new String[] {ruleFizzResult(), ruleBuzzResult()};
}

private String getComponentRuleResult(String[] results) {
    return Arrays.stream(results)
        .filter(v -> !v.isEmpty())
        .reduce(String::concat)
        .orElse(String.valueOf(this.value));
}

private String ruleFizzResult() {
    if (isDivBy(3)) return "Fizz";
    return "";
}

private String ruleBuzzResult() {
    if (isDivBy(5)) return "Buzz";
    return "";
}

private boolean isDivBy(int i) {
    return this.value % i == 0;
}

第 8 组操作:分离原子规则职责

使用Extract Class提炼类

  • getAtomicRuleResult中添加规则工厂的调用
private String[] getAtomicRuleResult() {
    List<Executable> rules = Rules.all();
    return new String[] {ruleFizzResult(), ruleBuzzResult()};
}
  • 使用Alt+Enter快速生成Executable接口、Rules类和all静态方法
  • 使用新的实现替换旧的实现
private String[] getAtomicRuleResult() {
    List<Executable> rules = Rules.all();
    return rules.stream()
            .map(rule -> rule.exec(this.value))
            .toArray(String[]::new);
}
  • 使用Alt+Enter快速生成Executable接口的exec方法
public interface Executable {
    String exec(int i);
}
  • 实现Rules.all,添加创建规则的工厂类和方法调用
public class Rules {

    public static List<Executable> all() {
        return Arrays.asList(
                DivRule.create(3, "Fizz"),
                DivRule.create(5, "Buzz")
        );
    }
}
  • 使用Alt+Enter快速生成DivRule类和create方法
  • 实现create方法和重载的exec方法,代码如下
public class DivRule implements Executable {
    private int input;
    private final String output;

    public DivRule(int in, String out) {
        this.input = in;
        this.output = out;
    }

    public static Executable create(int in, String out) {
        return new DivRule(in, out);
    }

    @Override
    public String exec(int i) {
        if (i % this.input == 0) return this.output;
        return "";
    }
}
  • 删除FizzBuzz中不需要的代码,剩余代码如下
@Override
public String toString() {
    String[] results = getAtomicRuleResult();
    return getComponentRuleResult(results);
}

private String[] getAtomicRuleResult() {
    List<Executable> rules = Rules.all();
    return rules.stream()
            .map(rule -> rule.exec(this.value))
            .toArray(String[]::new);
}

private String getComponentRuleResult(String[] results) {
    return Arrays.stream(results)
        .filter(v -> !v.isEmpty())
        .reduce(String::concat)
        .orElse(String.valueOf(this.value));
}
  • 重构toString使用Inline Method将函数内联化,消除额外的 2 个函数
@Override
public String toString() {
    return Rules.all()
        .stream()
        .map(rule -> rule.exec(this.value))
        .filter(v -> !v.isEmpty())
        .reduce(String::concat)
        .orElse(String.valueOf(this.value));
}

Over

TDD 和重构练习-FizzBuzz Sprint 2 - 快速变更
TDD 和重构练习-FizzBuzz Sprint 3 - 集成测试

「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」,让帮助程序员更加快乐高效地编程!

admin mark as excellent topic. 04 Jan 00:40
You need to Sign in before reply, if you don't have an account, please Sign up first.