一个完整的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:频繁、小规模的修改代码 

相关文章
|
流计算 SQL API
一文读懂Apache Flink发展史
本文整理自开源大数据专场中阿里巴巴高级技术专家杨克特(鲁尼)先生的精彩演讲,主要讲解了Apache Flink过去和现在的发展情况,同时分享了对Apache Flink未来发展方向的理解。
8275 0
|
消息中间件 Java Kafka
Java消息队列总结只需一篇解决ActiveMQ、RabbitMQ、ZeroMQ、Kafka
  一、消息队列概述 消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
4013 0
|
12月前
|
安全 Linux iOS开发
Cisco Secure Client 5.1.13.177 发布,新增功能简介
Cisco Secure Client 5.1.13.177 (macOS, Linux, Windows & iOS, Android) - 远程访问客户端
855 0
Cisco Secure Client 5.1.13.177 发布,新增功能简介
|
人工智能 监控 安全
06_LLM安全与伦理:部署大模型的防护指南
随着大型语言模型(LLM)在各行业的广泛应用,其安全风险和伦理问题日益凸显。2025年,全球LLM市场规模已超过6400亿美元,年复合增长率达30.4%,但与之相伴的是安全威胁的复杂化和伦理挑战的多元化
1057 0
|
存储 弹性计算 运维
阿里云服务器快照是什么?快照详细介绍
阿里云ECS快照服务是对云盘数据在某一时刻的完整备份,支持定时自动备份,防范数据丢失风险。快照按存储空间收费,可用OSS存储包抵扣。适用于日常备份、高危操作恢复、快速数据恢复、业务批量部署等场景,保障业务连续性与数据安全。
|
人工智能 云计算 决策智能
百望股份接入千问3,首个财税垂类MCP服务上线
近日,智能财税龙头企业百望股份与阿里云签署全面战略合作协议,共同成立“数据智能联合实验室”。双方将深化云计算与数据智能融合,以大模型为创新方向,首个深度融合通义千问Qwen3的财税行业MCP服务已在阿里云百炼上线。百望股份基于高质量数据推出交易管理、经营决策等智能体,助力企业释放数据价值。此次合作旨在构建全周期服务闭环,推动交易管理从“经验驱动”迈向“数据智能驱动”。
1007 12
|
运维 监控 Ubuntu
新书速览|Ubuntu Linux运维从零开始学
本文摘自《Ubuntu Linux运维从零开始学》,获出版社和作者授权发布。
|
人工智能 运维 监控
容器服务Kubernetes场景下可观测体系生产级最佳实践
阿里云容器服务团队在2024年继续蝉联Gartner亚洲唯一全球领导者象限,其可观测体系是运维的核心能力之一。该体系涵盖重保运维、大规模集群稳定性、业务异常诊断等场景,特别是在AI和GPU场景下提供了全面的观测解决方案。通过Tracing、Metric和Log等技术,阿里云增强了对容器网络、存储及多集群架构的监控能力,帮助客户实现高效运维和成本优化。未来,结合AI助手,将进一步提升问题定位和解决效率,缩短MTTR,助力构建智能运维体系。
|
前端开发 JavaScript API
技术经验分享:axios的全局配置baseUrl
技术经验分享:axios的全局配置baseUrl
991 3
|
存储 IDE 开发工具
Python中变量命名规则
【8月更文挑战第5天】
1493 4