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

时间:2022-06-18
本文章向大家介绍一个完整的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知识

1

TDD核心

红:测试失败

绿:测试通过

重构:优化代码和测试

2

TDD三大定律

该定律由Robert Martin提出:

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

3

FIRST原则

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

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

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

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

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

——The End——