一个完整的TDD演练案例(完)

简介: 一个完整的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知识


1TDD核心


红:测试失败

绿:测试通过

重构:优化代码和测试

2 TDD三大定律

该定律由Robert Martin提出:

  • 没有测试之前不要写任何功能代码
  • 只编写恰好能够体现一个失败情况的测试代码
  • 只编写恰好能通过测试的功能代码


3 FIRST原则

Fast: 测试要非常快,每秒能执行几百或几千个

Isolated:测试应能够清楚的隔离一个失败

Repeatable:测试应可重复运行,且每次都以同样的方式成功或失败

Self-verifying:测试要无歧义的表达成功或失败

Timely:频繁、小规模的修改代码 

相关文章
|
29天前
|
测试技术 开发者 运维
开发与运维测试问题之单元测试过程如何解决
开发与运维测试问题之单元测试过程如何解决
|
3月前
|
算法 测试技术 开发者
软件质量保证与测试知识点总结
【2月更文挑战第21天】软件质量保证与测试知识点总结
151 0
|
3月前
|
运维 测试技术 程序员
集成测试如何做?
集成测试如何做?
142 0
|
11月前
|
Java 测试技术 数据安全/隐私保护
软件测试小白如何实施单元测试?
软件测试小白如何实施单元测试?
|
自然语言处理 算法 IDE
一个完整的TDD演练案例(一)
一个完整的TDD演练案例(一)
一个完整的TDD演练案例(一)
|
运维 监控 NoSQL
性能测试从零开始实施指南——测试计划篇
首先要阐述本次性能测试的背景,即被测系统类型,面向哪些用户,具备什么特点,为什么要进行性能测试,预期的一些指标等等。
性能测试从零开始实施指南——测试计划篇
同学,你还不知道什么是混沌测试吗?
同学,你还不知道什么是混沌测试吗?
|
IDE Java 测试技术
一个完整的TDD演练案例(二)
一个完整的TDD演练案例(二)
|
IDE 测试技术 Go
一个完整的TDD演练案例(三)
一个完整的TDD演练案例(三)
|
XML 设计模式 前端开发
一个完整的TDD演练案例(四)
一个完整的TDD演练案例(四)