TDD从何开始

万事开头难。在TDD中,人们纠结最多的可能是这样一个问题:如何写第一个测试呢?实际上要视不同的问题而定。如果问题本身是一个算法求解,或者是一个大系统中的小单元,那么可以从最简单、最直观的情况出发,这样有助于快速建立信心,形成反馈周期。但是在实际的开发中,很多时候我们拿到的就是一个“应用级”的需求:一个网上订票系统,一个网上书店,愤怒的小鸟,诸如此类。此时,我们如何TDD呢?一种很自然的想法是:

  先对系统做简单的功能分解,形成概念中的相互协作的小模块。然后再从其中的一个小模块开始(往往是最核心的业务模块)TDD。我们把这种方式权且称为inside-out,也就是从部分到整体。这种方式可能存在的风险是:即使各个部分都通过TDD的方式驱动出来,我们也不能保证它们一起协作就能是我们想要的那个整体。更糟糕的是,直到我们把各个部分完成之前,我们都不知道这种无法形成整体的风险有多大。因此这对我们那个“概念中模块设计”提出了很高的要求,并且无论我们当前在实现哪个模块,都必须保证那个模块是要符合概念中的设计的。

  如果换一种思路呢?与其做概念中的设计,不如做真正的设计,通过写测试的方式驱动出系统的各个主要模块及其交互关系,当测试完成并通过,整个应用的“骨架”也就形成了。

  例如,现在假设我们拿到一个需求,要实现一个猜数字的游戏。游戏的规则很简单,游戏开始后随机产生4位不相同的数字(0-9),玩家在6次之内猜出这个4位数就算赢,否则就算输。每次玩家猜一个4位数,游戏都会告诉玩家这个4位数与正确结果的匹配情况,以xAyB的形式输出,其中x表示数字在结果中出现,并且出现的位置也正确,y表示数字在结果中出现但位置不正确。如果玩家猜出了正确的结果,游戏结束并输出“You win”,如果玩家输,游戏结束并输出“You lose”。

  针对这样一个小游戏,有人觉得简单,有人觉得复杂,但无论如何我们都没有办法一眼就看到整个问题的解决方案。因此我们需要理解需求,分析系统的功能:这里需要一个输入模块,那里需要一个随机数产生模块,停!既然已经在做分析了,为什么不用测试来记录这一过程呢?当测试完成的时候,我们的分析过程也就完成了。

  好吧,从何开始呢?TDD有一个很重要的原则-反馈周期,反馈周期不能太长,这样才能合理的控制整个TDD的节奏。因此我们不妨站在玩家的角度,从最简单的游戏过程开始吧。

  最简单的游戏过程是什么呢?游戏产生4位数,玩家一把猜中,You win,游戏结束。

  现在开始写这个测试吧。有一个游戏(Game),游戏开始(start):

Game game =newGame(); game.start();

  等等,似乎少了什么,是的,为了产生随机数,需要有一个AnswerGenerator;为了拿到用户输入,需要有一个InputCollector;为了对玩家的输入进行判断,需要有一个Guesser;为了输出结果,需要有一个OutputPrinter。真的要一口气创建这么多类,并一一实现它们吗?还好有mock,它可以帮助我们快速的创建一些假的对象。这里我们使用JMock2:

Mockery context = new JUnit4Mockery() {                       
    {                                              
        setImposteriser(ClassImposteriser.INSTANCE);
    }                                              
};                                                 
final AnswerGenerator answerGenerator = context.mock(AnswerGenerator.class);

  然后我们测试里的Game就变成这个样子了:

Game game =newGame(answerGenerator, inputCollector, guesser, outputPrinter); game.start();

  注意到这里为了通过编译,需要定义上面提到的几个类,我们不妨以最快的方式给出空实现吧:


public class AnswerGenerator {
   
}

public class InputCollector {
   
}

public class Guesser {
   
}

public class OutputPrinter {
   
}

  以及为了通过编译而需要的Game的最简单版本:

public class Game {
    public Game(AnswerGenerator generator, InputCollector inputCollector, Guesser guesser, OutputPrinter outputPrinter) {
       
    }
   
    public void start() {
       
    }
}

  好了,下面可以走我们的那个最简单的流程了。首先是由answerGenerator产生一个4位数,不妨假定是1234:

context.checking(new Expectations() {   
    {                                   
        one(answerGenerator).generate();
        will(returnValue("1234"));      
    } 
});

  这里需要我们的generator有一个generate方法,我们给一个最简单的空实现:

public class AnswerGenerator {     public String generate() {         return null;     }  }

  然后玩家猜数字,第一次猜了1234:

context.checking(new Expectations() {                   
                                                    
    // ...
                                                    
    {                                                   
        one(inputCollector).guess();                    
        will(returnValue("1234"));                      
    }                                                   

  为了使编译通过我们给inputCollector加上一个空的guess方法:

public class InputCollector {     public String guess() {         return null;     } }

  然后guesser判断结果,由于完全猜对,因此返回4A0B:


context.checking(new Expectations() {                  

    // ...                                               
                                                        
    {                                                   
        oneOf(guesser).verify(with(equal("1234")), with(equal("1234")));                    
        will(returnValue("4A0B"));                      
    }                                                  
}

  同理我们可以推出guesser的一个最简实现:

public class Guesser {     public String verify(String input, String answer) {         return null;     } }

  最后玩家赢,游戏输出“You win”,game over:


context.checking(new Expectations() {  

    // ...

    {                                   
        oneOf(outputPrinter).print(with(equal("You win")));     
    }                                   

  对应的outputPrinter可以做如下的微调:

public class OutputPrinter {     public void print(String result) {      } }

  最后别忘了启动Expectation验证:

context.assertIsSatisfied();

  整个测试方法现在看起来应该是这样的:

@Test                                                                             
 public void should_play_game_and_win() {                                          
     Mockery context = new JUnit4Mockery() {                                       
         {                                                                         
             setImposteriser(ClassImposteriser.INSTANCE);                          
         }                                                                         
     };                                                                            
     final AnswerGenerator answerGenerator = context.mock(AnswerGenerator.class);  
     final InputCollector inputCollector = context.mock(InputCollector.class);     
     final Guesser guesser = context.mock(Guesser.class);                          
     final OutputPrinter outputPrinter = context.mock(OutputPrinter.class);        
                                                                                   
     context.checking(new Expectations() {                                         
         {                                                                         
             one(answerGenerator).generate();                                      
             will(returnValue("1234"));                                            
         }                                                                         
                                                                                   
         {                                                                         
             one(inputCollector).guess();                                          
             will(returnValue("1234"));                                            
         }                                                                         
                                                                                   
         {                                                                         
             oneOf(guesser).verify(with(equal("1234")), with(equal("1234")));      
             will(returnValue("4A0B"));                                            
         }                                                                         
                                                                                   
         {                                                                         
             oneOf(outputPrinter).print(with(equal("You win")));                   
         }                                                                         
     });                                                                           
                                                                                   
     Game game = new Game(answerGenerator, inputCollector, guesser, outputPrinter);
     game.start();                                                                 
                                                                                   
     context.assertIsSatisfied();                                                  
 }

  运行测试,会看到下面的错误信息:


java.lang.AssertionError: not all expectations were satisfied

expectations:
expected once, never invoked: answerGenerator.generate(); returns "1234"
expected once, never invoked: inputCollector.guess(); returns "1234"
expected once, never invoked: guesser.verify("1234"); returns "4A0B"
expected once, never invoked: outputPrinter.print("You win"); returns a default value
at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

  太好了,正是我们期望的错误!别忘了我们只是在测试中定义了期望的游戏流程,真正的game.start()还是空的呢!现在就让测试指引着我们前行吧。

  先改一改我们的Game类,把需要依赖的协作对象作为Game的字段:


private AnswerGenerator answerGenerator;
private InputCollector inputCollector;
private Guesser guesser;
private OutputPrinter outputPrinter;

public Game(AnswerGenerator answerGenerator, InputCollector inputCollector, Guesser guesser, OutputPrinter outputPrinter) {
     this.answerGenerator = answerGenerator;
     this.inputCollector = inputCollector;
     this.guesser = guesser;
     this.outputPrinter = outputPrinter;
}

  然后在start方法中通过answerGenerator来产生一个4位数:

public void start() {                          
    String answer = answerGenerator.generate();
}

  再跑测试,会发现仍然错,但结果有变化,第一步已经变绿了!


java.lang.AssertionError: not all expectations were satisfied
expectations:
expected once, already invoked 1 time: answerGenerator.generate(); returns "1234"
expected once, never invoked: inputCollector.guess(); returns "1234"
expected once, never invoked: guesser.verify("1234"); returns "4A0B"
expected once, never invoked: outputPrinter.print("You win"); returns a default value
at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

  下面应该使用inputCollector来收集玩家的输入:

public void start() {                          
    String answer = answerGenerator.generate();
    String guess = inputCollector.guess();     
}

  跑测试,错但是结果进一步好转,已经有两步可以通过了:


 java.lang.AssertionError: not all expectations were satisfied
expectations:
expected once, already invoked 1 time: answerGenerator.generate(); returns "1234"
expected once, already invoked 1 time: inputCollector.guess(); returns "1234"
expected once, never invoked: guesser.verify("1234"); returns "4A0B"
expected once, never invoked: outputPrinter.print("You win"); returns a default value
at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

  下面加快节奏,按照测试中的需求把剩下的流程走通吧:

public void start() {                          
    String answer = answerGenerator.generate();
    String guess = inputCollector.guess();     
    String result = "";                        
    do {                                       
       result = guesser.verify(guess, answer); 
    } while (result != "4A0B");                
    outputPrinter.print("You win");            
}

  再跑测试,啊哈,终于看到那个久违的小绿条了!

  回顾一下这一轮从无到有、测试从红到绿的小迭代,我们最终的产出是:

  1、一个可以用来描述游戏流程的测试(需求,文档?)。

  2、由该需求推出的一个流程骨架(Game.start)。

  3、一堆基于该骨架的协作类,虽然是空的,但它们每个的职责是清晰的。

  经过这最艰难的第一步(实际上叙述的过程比较冗长,但反馈周期还是很快的),相信每个人都会对完整实现这个游戏建立信心,并且应该知道后面的步骤要怎么走了吧。是的,我们可以通过写更多的骨架测试来进一步完善它(比如考虑失败情况下的输出,增加对用户输入的验证等等),或者深入到每个小协作类中,继续以TDD的方式实现每一个协作类了。无论如何,骨架已在,我们是不大可能出现大的偏差了。

====================================分割线================================

最新内容请见作者的GitHub页:http://qaseven.github.io/

时间: 2025-01-21 05:37:51

TDD从何开始的相关文章

TDD并不是看上去的那么美

出处:http://coolshell.cn/articles/3649.html 春节前的一篇那些炒作过度的技术和概念中对敏捷和中国ThoughtWorks的微辞引发了很多争议,也惊动了中国ThoughtWorks公司给我发来了邮件想来找我当面聊聊.对于Agile的Fans们,意料之中地也对我进行了很多质疑和批评.我也回复了许多评论.不过,我的那些回复都是关于中国ThoughtWorks咨询师以及其咨询的方法的.我对Agile方法论中的具体内容评价的不是很多,所以,我想不妨讨论一下Agile方

用phpUnit入门TDD

从一个银行账户开始 假设你已经 安装了phpunit.   我们从一个简单的银行账户的例子开始了解TDD(Test-Driven-Development)的思想.   在工程目录下建立两个目录, src和test,在src下建立文件 BankAccount.php,在test目录下建立文件BankAccountTest.php.   按照TDD的思想,我们先写测试,再写生产代码,因此BankAccount.php留空,我们先写BankAccountTest.php.   <?php class

用NUnit2.1简单实现.net的测试驱动开发(TDD)

用NUnit2.1简单实现.net的测试驱动开发(TDD)下面的例子很简单,就是实现两个整数的四则运算,TDD提倡测试优先,即先写测试用例,再写运行代码,刚下了个NUnit2.1,迫不及待的试了试--1最初的测试用例using System;using NUnit.Framework;namespace netshop{ /// <summary> /// 四则运算TestCls测试用例 /// Edit by spgoal /// </summary> [TestFixture]

敏捷测试(1) TDD概念

题记 本系列笔记将从测试人员的角度,总结在百度两年来的测试经验,记录一个完整的基于敏捷流程的验收测试全过程,分享在测试过程中的一些知识和经验,以及自己的一些理念.总结自己,也希望对大家有益. 概念 验收测试驱动开发(ATDD)和测试驱动开发(TDD)是完全不同的两个概念. TDD更偏重自动化case先行,而ATDD更偏重于验收细节.质量标准先行. 在了解ATDD之前,先回顾下TDD: 测试驱动开发(TDD) 极限编程的方法之一,从业务入手,以测试先行的方法来反向推动代码的实现.那什么是TDD呢?

网络相册开发(3)——测试驱动开发(TDD)

测试驱动开发的理论已经提出好多年了,在这里关于他的原理和优势我就不多说了,我将大略的写一下我在实际中运用TDD的过程. 补一个jar: commons-pool-1.4.jar 过程 1.搭建测试用例运行环境 2.编写接口类 3.针对接口类编写测试用例 4.实现接口类,编写对应的功能代码 5.运行测试 6.如不通过,修改直至通过 7.循环完成其他功能 搭建测试用例运行环境 spring采用的依赖注射技术带来的一个主要好处就是你的代码对容器的依赖性比传统的J2EE开发小得多.配合spring提供的

TDD中的单元测试写多少才够

测试驱动开发(TDD)已经是耳熟能详的名词,既然是测试驱动,那么测试用例代码就要写在开发代码的前面.但是如何写测试用例?写多少测试用例才够?我想大家在实际的操作过程都会产生这样的疑问. 3月15日,我参加了thoughtworks组织的"结对编程和TDD Openworkshop"活动,聆听了tw的资深咨询专家仝(tong2)键的精彩讲解,并在讲师的带领下实际参与了一次TDD和结对编程的过程.活动中,仝键老师对到底写多少测试用例才够的问题,给出了下面一个解释: 我们写单元测试,有一个重

如何开始TDD

TDD已经被证实为一项可以提高软件质量的基本实践,然而对于很多程序员来说,在抱着尝试一下的想 法实践的时候,却困难重重.这里面有多方面的因素,比如环境,比如编程习惯,比如不会写测试用例等 等.TDD是一项实践性很强的事,就像OO一样需要大量的实践来获得经验,因此如果能在平时养成写测试 的习惯,从简单到复杂一点一点进行练习,就能慢慢的掌握TDD了.这里建议初学的人可以考虑先写代码 后写测试,等到测试写的很熟练了再转到先写测试后写代码的阶段. 先说说如何先写代码再写测试: 我看到很多人在学习新技术或

使用WatiN进行TDD

这两天听说了一个很不错的基于.NET平台的Web自动化测试框架WatiN,下载试用了一下,的确很好用 .它的基本功能和Selenium有点像,但是不如Selenium强大,没有脚本录制,只支持IE6/7等.它基本功 能包括自动操作大部分的HTML元素,多种查找方式,支持AJAX,支持frame/iframe,支持弹出框等等.现 在用一个简单的例子来看看怎样使用WatiN来进行TDD. 在这个例子中,基于Northwind数据库实现这样一个功能,通过ID查找某个Customer,列出相关基本信 息

在ASP.NET MVC中进行TDD开发

TDD介绍 TDD是一种开发方法,全称是Test-Driven development,中文是测试驱动开发.作者是Kent Beck.首先让我介绍一下三种常见的开发方式: 第一种:先Coding,然后Bug Fix. 第二种:先Coding,然后Unit Test,最后Bug Fix.很显然用了单元测试的比第一种开发方式要好不少. 第三种:就是本文要说的TDD,它的方式和第二种恰恰相反.TDD先设计单元测试,然后再Coding,最后修复Bug.看下图: TDD开发过程可以看成:给制自己制定一个目

FDD和TDD都弱爆了,看看最牛的NDD!

  话说FDD和TDD两兄弟为了争宠吵得不可开交时,突然从石头里蹦出个NDD,宣称自己才是世上最牛的"DD",FDD和TDD两兄弟顿时惊慌失措.黯然失色,私下里打探了一下NDD的底细.今天我们就来揭揭NDD的底,看看他到底牛在哪里? 首先要从双工方式说起. FDD(频分双工):采用两个对称的频率信道来分别发射和接收信号. TDD(时分双工):发射和接收信号是在同一频率信道的不同时隙中进行的. 有人说,FDD是全双工,TDD是半双工.从一定意义上,这两个都不是全双工,因为都不能实现在同一