TDD和重构练习-FizzBuzz Sprint 1

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 - 集成测试

联系我
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容