For
Kobe Bryant details how Kevin Durant can get even better | 'Detail' Excerpt | ESPN
下面以 3 分钟为时限来组织操作,每一组完成一个明确的目标。
创建工程和配置
创建工程
IntelliJ IDEA Community Edition 2019.1.1 x64
Java8
Gradle
配置单元测试
步骤 | 动作 | 结果 | 用时 |
---|---|---|---|
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
一般会自动刷新,也可手动刷新:在Build
和Gradle
窗口的左上角都有该刷新按钮。
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);
借助 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;
}
步骤 | 动作 | 结果 | 用时 |
---|---|---|---|
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());
}
为该 PBI 添加测试数据,然后运行测试,红色
测试方法的CsvSource
内增加一条数据:"5, Buzz"
快速让测试通过
FizzBuzz
类toString
方法内增加新逻辑,代码如下,然后运行测试,绿色
@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;
}
为该 PBI 添加测试数据
测试方法的CsvSource
内增加一条数据:"15, FizzBuzz"
快速让测试通过
FizzBuzz
类toString
方法内增加新逻辑
将光标移到到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
过长函数。
该重构过程需要步骤较多,故单独形成一组操作。
@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);
}
@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;
}
原子规则
职责使用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));
}
TDD 和重构练习-FizzBuzz Sprint 2 - 快速变更
TDD 和重构练习-FizzBuzz Sprint 3 - 集成测试
「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」,让帮助程序员更加快乐高效地编程!