开始第五个任务
在开始编写测试之前,先要深入分析该任务表达的需求信息。“判断游戏结果。判断猜测次数,如果满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知识
1TDD核心
红:测试失败
绿:测试通过
重构:优化代码和测试
2 TDD三大定律
该定律由Robert Martin提出:
- 没有测试之前不要写任何功能代码
- 只编写恰好能够体现一个失败情况的测试代码
- 只编写恰好能通过测试的功能代码
3 FIRST原则
Fast: 测试要非常快,每秒能执行几百或几千个
Isolated:测试应能够清楚的隔离一个失败
Repeatable:测试应可重复运行,且每次都以同样的方式成功或失败
Self-verifying:测试要无歧义的表达成功或失败
Timely:频繁、小规模的修改代码