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

简介: 一个完整的TDD演练案例(四)

第四个任务  



还剩下两个任务:

  • 记录并显示历史猜测数据
  • 判断游戏结果


究竟应该选择哪一个任务作为第四个任务,并没有定论。从业务逻辑看,“判断游戏结果”任务更重要,它才是整个游戏的核心逻辑。可从技术实现看,“判断游戏结果”可以依赖“记录并显示历史猜测数据”。因为分析“判断游戏结果”任务,实际上做了两件事:其一是判断猜测次数是否超过指定的6次;其二是判断每次猜测的结果。第二件事已经被我们开发的第二个任务覆盖。而对于测试次数而言,如果我们记录了历史猜测数据,那么这个次数也可以唾手可得。


讨论:测试驱动开发需要事先设计吗?



Martin Fowler的文章Is Design Dead?其实就是对此问题的正本清源。由于测试驱动开发提倡“测试先行,简单设计”,许多人就误认为TDD不需要设计,以讹传讹之下,甚至导致许多优秀的设计者抛弃了设计去实践TDD,最后得出TDD不可行的结论。


我个人认为,视场景而定,测试驱动开发仍可进行事先设计。设计并不仅包含技术层面的设计如对OO思想乃至设计模式的运用,它本身还包括对需求的分析与建模。若不分析需求就开始编写测试,就好像没有搞清楚要去的地方,就开始快步前行,最后发现南辕北辙。测试驱动开发提倡的任务分解,实际上就是一种需求的分析。而如何寻找职责,以及识别职责的承担者则可以视为建模设计。测试驱动更像是一种培养设计专注力的手段,就像冥想者通过盘腿静坐的手段来体悟天地一样,测试驱动可以强迫你站在测试的角度(就是使用者的角度)去思考接口,如此才能设计出表现意图的接口。但编写测试自身并不能取代设计,正如盘腿静坐并不等于就是冥想。


在开始测试驱动开发之前,做适度的事先设计,还有利于我们仔细思考技术实现的解决方案。它与测试驱动接口的设计并不相悖。解决方案或许属于实现层面,若过早思考实现,会干扰我们对接口的判断;但完全不理会实现,又可能导致设计方向的走偏。举例来说,如果我们要实现XML消息到Java对象的转换。一种解决方案是通过jaxb将消息转换为Java对象,然后再定义转换映射的Transformer,通过硬编码或者反射的方式将其转换为相关的领域对象。然后在执行了业务操作后,再将返回的结果转换为另一个Jaxb对象。而另一种解决方案则是通过引入模板,例如StringTemplate或者Velocity,定义转换的模板,然后进行替换实现。这两种解决方案的区别,直接影响了我们划分任务的方式。



我们选择“记录并显示历史猜测数据”作为第四个任务。同样,对于此任务,我们要事先考虑清楚,究竟应该由谁来承担这个职责?恩,注意,这里其实包含了两项任务:记录与显示。当我们看到类似“和”、“或者”等并列连接词时,都应该思考它是否表达了多个职责?因此,对于第四个任务,我们应该稍稍拆分一下,分解成两个任务:

  • 记录历史猜测数据;
  • 显示历史猜测数据;


那么应该谁来“记录历史猜测数据”?我们应该寻找承担该职责的对象。


知识:寻找职责的承担者



寻找职责的承担者,其实就是寻找某个可以承担该职责的角色。角色又是什么?想象我们现实世界中的角色。看看我们身边,是否角色遍地可寻?BA角色负责分析需求,DEV角色负责实现功能,QA角色负责测试功能是否正确,PM角色负责管理整个项目的进度与项目成员。我们是依据什么来划分角色的?——能力。能力的体现是什么?除了诸多素质要求,最直接的体现就是“知识”。因此,所谓“角色”,就是拥有了相关“知识”从而具有相关“能力”的人。


什么角色应该记录历史猜测数据呢?那就是要寻找谁具有记录历史猜测数据的能力。于是推之于知识,就是谁拥有每一次猜测的数据。显然,Game拥有当前猜测的数据,因此承担责任的应该为Game。


现在,开始编写测试。既然已经辨别出Game对象,就应该针对它编写测试方法,让我们还是从测试方法的业务逻辑描述开始吧:


public class GameTest {    
   private final Answer actualAnswer = Answer.createAnswer("1 2 3 4");    
   private Game game;    
   @Before    
   public void setUp() throws Exception {        
       AnswerGenerator answerGenerator = mock(AnswerGenerator.class);      
       when(answerGenerator.generate()).thenReturn(actualAnswer);        
       game = new Game(answerGenerator);    
   }    
   @Test    
   public void should_record_every_guess_result() {      
       game.guess(Answer.createAnswer("2 1 6 7"));      
       game.guess(Answer.createAnswer("1 2 3 4"));      
       List<GuessResult> guessHistory = game.guessHistory();        
       assertThat(guessResults.size(), is(2));        
       assertThat(guessResults.get(0).result(), is("0A2B"));        
       assertThat(guessResults.get(0).inputAnswer().toString(), is("2 1 6 7"));        
       assertThat(guessResults.get(1).result(), is("4A0B"));        
       assertThat(guessResults.get(1).inputAnswer().toString(), is("1 2 3 4"));    
   }
}


在这里,实际上我驱动出了Game的guessHistory()方法,同时还得到了一个封装了猜测结果的GuessResult对象。与第一个任务不同的是,我没有使用字符串来表示猜测结果,这是因为这里的历史猜测数据不仅包含了猜测结果,还包含了当前的测测数据。


现在,应该考虑“显示历史猜测记录”的任务了。这个功能就是要在猜测了数字之后,在控制台显示历史猜测记录。虽然是控制台,我们仍然认为这属于界面的工作。TDD根本就不应该用来驱动界面设计,还是将注意力放到业务逻辑上来吧。抛开界面,这里的逻辑就转换为:

当用户猜测了数字后,应该显示历史猜测记录。


将界面与业务逻辑分开体现了“关注点分离”原则,也是表现层设计的常用做法。最常见的处理界面设计的模式就是MVC模式。因此在这里可以引入GameController类,就目前而言,它可以负责Game与GameView的协作,所以相应的还可以为界面显示定义一个专属的View对象。


虽然在这里是用控制台显示历史猜测数据信息,实现非常简单,直接调用System.out.println()方法即可,然而我们却很难测试控制台是否显示了该信息。虽然有一些框架也提供了Mock控制台的功能,但就TDD而言,这样的测试并无实际意义。我们需要合理地辨别在功能实现中,哪些内容适合编写自动化测试,哪些内容适合人工测试。因此,这里可以引入Mock框架来模拟GameView,我们只需验证Controller与View之间的协作即可。这时,测试还有助于我们设计出可测试性好的类。


因为是Controller,需要接受用户输入,而非直接传入答案的字符串值。同理,我们在TDD中也不可能测试业务逻辑与控制台的交互。因此,同样需要引入InputCommand类型来封装输入逻辑,然后以Mock框架来模拟InputCommand。 故而,我们为该功能编写的测试为:


public class GameControllerTest {    
   @Mock    
   private GameView mockGameView;    
   @Mock    
   private InputCommand mockCommand;    
   @Mock    
   private AnswerGenerator mockGenerator;    
   private Game game;    
   private Answer correctAnswer;    
   private Answer errorAnswer;    
   private GameController gameController;    
   @Before    
   public void setUp() throws Exception {        
       MockitoAnnotations.initMocks(this);      
       correctAnswer = Answer.createAnswer("1 2 3 4");        
       errorAnswer = Answer.createAnswer("1 2 5 6");  
       when(mockGenerator.generate()).thenReturn(correctAnswer);        
       game = new Game(mockGenerator);        
       gameController = new GameController(game, mockGameView);    
   }        
   @Test    
   public void should_display_guess_history_message_when_guess_number_twice() {        
       //given        
       when(mockCommand.input()).thenReturn(errorAnswer);        
       GameController gameController = new GameController(game, mockGameView);  
       //when        
       gameController.play(mockCommand);        
       //then      
       verify(mockGameView).showGuessHistory(anyList());    
   }
}


在编写该测试之前,我们实则做了一部分设计与分析工作,辨别各种职责以及承担这些职责的对象,尤其重要的是,要分辨出它们之间的协作方式。对协作的分析应以被测对象为主。一旦分析清楚,就应该编写测试,通过测试来驱动对象之间的协作方式。在编写的测试中,参与协作的其他对象都可以通过Mock来模拟,不一定要有实现,只需体现它们的接口即可。


例如,在当前这个测试中,除了之前已经处理过的Game与AnswerGenerator之间的协作外,我主要考虑了InputCommand与GameView之间的协作方式,其中包括:三者之间的依赖注入,例如GameView作为构造函数的参数,因为一个GameController对象应对应一个GameView对象;而InputCommand则作为play()方法的输入参数。这里的GameController的接口就是通过测试驱动获得的。由于我们测试的是历史猜测结果是否显示,因此使用了Mockito框架的verify方法对这种对象之间的协作进行了验证。之所以在验证逻辑中没有验证具体的猜测结果是否正确,是因为这个逻辑已经在Game的测试中覆盖;而对于GameController,我们需要验证的逻辑只限于“是否显示历史猜测数据”,而非“显示了什么样的历史猜测数据”。


注意:这里创建了多个Mock对象,因此使用了Mockito提供的@Mock便捷方式来创建这些Mock对象。


InputCommand可以定义为接口,真正的控制台实现交给了ConsoleInputCommand类。实现如下:


public class ConsoleInputCommand implements InputCommand {    
   private BufferedReader bufferedReader;    
   {        
       bufferedReader = new BufferedReader(new InputStreamReader(System.in));    
   }    
   @Override    
   public Answer input() {        
       try {            
           String inputAnswer = bufferedReader.readLine();            
           return Answer.createAnswer(inputAnswer);        
       } catch (IOException e) {            
           throw new RuntimeException(e.getMessage());        
       }    
   }
}
相关文章
|
4月前
|
Devops Java 测试技术
软件测试/测试开发|常见软件测试框架类型:TDD、BDD、DDD、ATDD、DevOps介绍
软件测试/测试开发|常见软件测试框架类型:TDD、BDD、DDD、ATDD、DevOps介绍
68 0
|
2月前
|
缓存 前端开发 JavaScript
前端项目重构的一些思考和复盘
前端项目重构的一些思考和复盘
53 1
|
3月前
|
运维 测试技术 程序员
集成测试如何做?
集成测试如何做?
|
3月前
|
监控 测试技术
如何使用PDCA来改进测试流程?
如何使用PDCA来改进测试流程?
|
4月前
|
测试技术 UED
软件测试/测试开发|如何使用场景法设计测试用例?
软件测试/测试开发|如何使用场景法设计测试用例?
71 0
|
7月前
|
测试技术 数据库
接口并发性能测试开发之:从测试方案设计、测试策略、指标分析到代码编写,这一篇全搞定。
接口并发性能测试开发之:从测试方案设计、测试策略、指标分析到代码编写,这一篇全搞定。
154 0
|
9月前
|
前端开发 JavaScript Java
TDD测试驱动开发案例【水货】
TDD测试驱动开发案例【水货】
|
10月前
|
存储 Java 测试技术
【C#编程最佳实践 一】单元测试实践
【C#编程最佳实践 一】单元测试实践
72 0
|
11月前
|
Java 测试技术 程序员
「敏捷架构」核心实践:测试驱动开发(TDD)简介
「敏捷架构」核心实践:测试驱动开发(TDD)简介
|
敏捷开发 监控 安全
测试思想-测试流程 测试流程简述
测试思想-测试流程 测试流程简述
233 0