一个完整的TDD演练案例(完)
逸言 | 逸派胡言
测试驱动开发完整案例的最后一部分,除完成了整个案例的测试驱动之外,还介绍了依赖注入以及测试驱动开发的定律与原则。
开始第五个任务
在开始编写测试之前,先要深入分析该任务表达的需求信息。“判断游戏结果。判断猜测次数,如果满6次但是未猜对则判负;如果在6次内猜测的4个数字值与位置都正确,则判胜。”实际上这里引入了对游戏猜测的控制逻辑,主要是对猜测次数的控制。这样的控制逻辑应该交给谁呢?
多数时候,程序员容易将这样的控制逻辑放到主程序入口处,即main()函数中。这并非恰当的方式。一方面,这里的控制逻辑仍然属于业务逻辑的范畴,不应该暴露给调用者,同时也加大了调用者的负担;另一方面,倘若程序不再作为控制台程序时,例如编写Web Application,主程序入口的内容就要调整,甚至导致这一逻辑的重复。
有了编写第四个任务作为基础,我们很容易判断出该控制逻辑应该交给GameController。编写测试也变得简单:
public class GameControllerTest {
@Test
public void should_end_game_and_display_sucessful_message_when_number_is_correct_in_first_round() {
//given
when(mockCommand.input()).thenReturn(correctAnswer);
//when
gameController.play(mockCommand);
//then
verify(mockCommand, times(1)).input();
verify(mockGameView).showMessage("successful");
}
@Test
public void should_end_game_and_display_failure_message_once_times_reach_max_times() {
//given
when(mockCommand.input()).thenReturn(errorAnswer);
GameController gameController = new GameController(game, mockGameView);
//when
gameController.play(mockCommand);
//then
verify(mockCommand, times(6)).input();
verify(mockGameView).showMessage("failed");
}
}
这里的两个测试与第四个任务测试“显示历史猜测数据”任务的测试相似,唯一不同的是我们添加了对InputCommand协作的验证,并以Mockito提供的times()方法准确的验证了调用的次数。默认情况下,verify验证的次数为1,但我在第一个测试中仍然给出了times(1),是希望在测试中明确的表示它被执行了一次。
通过编写测试,我们驱动出了GameController、InputCommand与GameView之间的协作关系,并且还驱动出showMessage()方法。如果你觉得showMessage()方法的定义太过宽泛,也可以定义showFailure()和showSuccess()方法来体现这里表达的业务逻辑。
GameController的实现就变简单了:
public class GameController {
private static final int MAX_TIMES = 6;
private Game game;
private GameView gameView;
public GameController(Game game, GameView gameView) {
this.game = game;
this.gameView = gameView;
}
public void play(InputCommand inputCommand) {
GuessResult guessResult;
do {
Answer inputAnswer = inputCommand.input();
guessResult = game.guess(inputAnswer);
gameView.showCurrentResult(guessResult);
gameView.showGuessHistory(game.guessHistory());
} while (!guessResult.correct() && game.guessHistory().size() < MAX_TIMES);
gameView.showMessage(guessResult.correct() ? "successful" : "failed");
gameView.showMessage("The correct number is " + game.actualAnswer());
}
}
运用依赖注入框架
至此,我们的程序基本完成。我们定义并实现了各个参与协作的类,但是,我们需要管理类之间的依赖,组合这些相关的对象。由于我们采用了测试驱动,因此比较好的保证了各个类的可测试性,而达成可测试性的诀窍就是“依赖注入”。
知识:依赖注入
依赖注入模式体现了“面向接口设计”原则,即分离接口与实现,并通过构造函数注入、设值方法注入或接口注入等手法将外部依赖注入到一个类中,从而解除该类与它协作的外部类之间的依赖。具体类型参考Martin Fowler的文章Inversion of Control Containers and the Dependency Injection pattern。
在我们的例子中,主要通过构造函数注入的方式实现依赖注入。我们当然可以自己来组合这些类,但也可以运用现有的框架,例如Java平台下的Spring以及更轻量级的Guice。
在目前的设计中,我们仅仅针对GameView以及InputCommand进行了接口与实现分离。由于InputCommand是作为play()方法的传入参数,不在依赖管理范围之内。至于RandomIntGenerator以及AnswerGenerator则是通过类直接注入的,因此,我们仅需做如下调整。
首先为那些运用了构造函数注入的类配置Guice提供的@Inject,如下所示:
public class AnswerGenerator {
private RandomIntGenerator randomIntGenerator;
@Inject
public AnswerGenerator(RandomIntGenerator randomIntGenerator) {
this.randomIntGenerator = randomIntGenerator;
}
}
public class Game {
private Answer actualAnswer;
private final ArrayList<GuessResult> guessHistory;
@Inject
public Game(AnswerGenerator answerGenerator) {
this.actualAnswer = answerGenerator.generate();
guessHistory = new ArrayList<GuessResult>();
}
}
public class GameController {
private static final int MAX_TIMES = 6;
private Game game;
private GameView gameView;
@Inject
public GameController(Game game, GameView gameView) {
this.game = game;
this.gameView = gameView;
}
}
对于GameView接口,在默认情况下,Guice框架并不知道该注入它的哪个实现类(即使此时只有一个实现类),因此需要创建一个Module,它派生自Guice提供的AbstractModule,能够将接口与实现类进行绑定:
public class GuessNumberModule extends AbstractModule {
@Override
protected void configure() {
bind(GameView.class).to(ConsoleGameView.class);
}
}
现在在main()函数中就无需进行繁琐的类型间组合,Guice框架会帮我们完成依赖对象之间的注入。唯一需要做的是创建一个Injector对象,通过它可以获得我们需要的GameController实例:
public class GuessNumber {
public static void main(String[] args) {
Injector injector = createInjector(new GuessNumberModule());
GameController gameController = injector.getInstance(GameController.class);
InputCommand command = new ConsoleInputCommand();
System.out.println("Please input four numbers following by X X X X(0--9)");
gameController.play(command);
}
}
TDD知识
1
TDD核心
红:测试失败
绿:测试通过
重构:优化代码和测试
2
TDD三大定律
该定律由Robert Martin提出:
- 没有测试之前不要写任何功能代码
- 只编写恰好能够体现一个失败情况的测试代码
- 只编写恰好能通过测试的功能代码
3
FIRST原则
Fast: 测试要非常快,每秒能执行几百或几千个
Isolated:测试应能够清楚的隔离一个失败
Repeatable:测试应可重复运行,且每次都以同样的方式成功或失败
Self-verifying:测试要无歧义的表达成功或失败
Timely:频繁、小规模的修改代码
——The End——
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- kubernete编排技术一:pod
- Golang逃逸分析
- kubernete编排技术二:deployment
- Gearman介绍、原理分析、实践改进
- 『JWT』,你必须了解的认证登录方案
- 静态网站
- ansible超详细使用指南
- Linux下快速安装Python3和pip
- 面试官:谈一谈java中基于AQS的并发锁原理
- kubernete编排技术三:StatefulSet
- Qt网络聊天室客户端
- 聊聊java中的StampedLock并发锁原理
- tomcat对AQS的扩展:使用LimitLatch控制连接数
- kubernete编排技术五:DaemonSet
- 深度剖析github上15.1k Star项目:redux-thunk