TDD by example (5) -- 后招

还是to-do list开头

  随机生成答案
  检查输入是否合法 
  判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负 
  如果4个数字全中,则判胜
  
  实现IRandomIntGenerator

如果4个数字全中,则判胜

首先还是以测试开始,与GameOver类似

[Test]
public void should_win_when_guess_correct()
{
    var mockObserver 
= new Mock<IGameObserver>();
    game.GameClear 
+= mockObserver.Object.GameClear;
    game.Guess(
new int[] {1234});
    mockObserver.Verify(m 
=> m.GameClear(), Times.Exactly(1));
}

然后在IGameObserver接口中添加GameClear方法,在Game类中添加GameClear事件

public interface IGameObserver
{
    
void GameOver();
    
void GameClear();
}

运行测试,失败。接下来添加代码让测试通过

public string Guess(int[] guess)
{
    
string result = GetGuessResult(guess);
    guessTimes
++;
    
if(IsGameClear(result))
    {
        OnGameClear();
    }
    
if(IsGameOver(result))
    {
        OnGameOver();
    }
    
return result;
}

private bool IsGameClear(string result)
{
    
return result == CorrectGuess;
}

protected void OnGameClear()
{
    
if (GameClear != null)
    {
        GameClear();
    }
}

接下来重构,我们看到在Guess方法中,如果猜测结果是错误的,那么在IsGameClear中判断答案是否正确后,还需要再次在IsGameOver中进行判断,因此修改此部分逻辑

if(IsGameClear(result))
{
    OnGameClear();
}
else if(IsGameOver(result))
{
    OnGameOver();
}

private bool IsGameOver(string result)
{
    
return guessTimes >= MaxGuessTimes;
}

在Guess中加了一个else,这样就不会每次都调用两个函数了,然后再IsGameOver中去掉了对答案的判断,因为if中已经可以保证调用IsGameOver时答案是不正确的。至此,IsGameOver的职责已经不是判断是否游戏结束,而是判断猜测次数是否超过了规定的次数,因此将函数名字改为CanContinueGuess,参数result也可以去掉了。

private bool CanContinueGuess()
{
    
return guessTimes >= MaxGuessTimes;
}


又完成了一个任务,更新to-do list

  随机生成答案
  检查输入是否合法 
  判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负 
  如果4个数字全中,则判胜
  
  实现IRandomIntGenerator

记录历史猜测数据并显示

这里还是要做一个决定,到底谁来负责记录历史,显然这是一个单独的功能,不应该是Game类的职责,因此我们需要另外的一个类GameHistory来做这件事。然后我们还要决定谁来调用GameHistory,一种方式是放在Game类德Guess方法里,还有一种是放在Game类外的某个调用Guess方法的方法中。这里我选择放在Guess方法中,那么需要测试每次调用Guess的时候,GameHistory中的方法被正确调用,测试代码如下:

[Test]
public void should_record_guess_history()
{
    var mockHistory 
= new Mock<IGameHistory>();
    var mockAnswerGenerator 
= new Mock<IAnswerGenerator>();
    mockAnswerGenerator.Setup(generator 
=> generator.Generate()).Returns(new[] {1234});
    var answerGenerator 
= mockAnswerGenerator.Object;
    game 
= new Game(answerGenerator, mockHistory.Object);

    game.Guess(
new int[] {5678});
    mockHistory.Verify(h 
=> h.Add(new int[] {5678}, "0A0B"));
}

然后,为了让代码编译通过,需要添加IGameHistory接口,并修改Game的构造函数,这里我没有修改原有的构造函数,而是添加了一个新的构造函数

public interface IGameHistory
{
    
void Add(int[] guess, string result);
}

public Game(IAnswerGenerator answerGenerator, IGameHistory gameHistory):this(answerGenerator)
{
    
this.gameHistory = gameHistory;
}

运行测试,失败。接下来添加代码

public string Guess(int[] guess)
{
    
string result = GetGuessResult(guess);
    gameHistory.Add(guess, result);
    guessTimes
++;
    
if (IsGameClear(result))
    {
        OnGameClear();
    }
    
else if (CanContinueGuess())
    {
        OnGameOver();
    }
    
return result;
}

每一次Guess被调用的时候,将猜测历史添加进去。运行测试,发现以前的测试都失败了,查找原因原来是gameHistory==null。这里我认为对于Game类来说gameHistory并不是一定要有的,因此我需要加上gameHistory是否为空的判断

public string Guess(int[] guess)
{
    
string result = GetGuessResult(guess);
    
if (gameHistory != null)
    {
        gameHistory.Add(guess, result);
    }
    guessTimes
++;
    
if (IsGameClear(result))
    {
        OnGameClear();
    }
    
else if (CanContinueGuess())
    {
        OnGameOver();
    }
    
return result;
}

 好了,全部测试都通过了。重构一下,将纪录history的代码提取到一个方法中。这里你也可以使用NullObject模式,详细代码就不写了。

如果你对GameOver和GameClear的情况不放心,也可以添加两个测试覆盖这两种情况,看看输和赢的时候GameHistory能不能被记录下来,这里就不写了。

最后的代码如下

Code

测试代码

Code

更新to-do list

  随机生成答案
  检查输入是否合法 
  判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负 
  如果4个数字全中,则判胜
  
  实现IRandomIntGenerator 
  实现IGameHistory
  显示猜测纪录

我们完成了记录历史数据的任务,但是显示的任务并未完成,同时我们还没有对IGameHistory进行实现, 因此又添加了两个任务

相关文章

如何开始TDD

测试驱动开发之可执行文档

三张卡片帮你记住TDD原则 

posted @ 2009-07-13 22:50  Nick Wang (懒人王)  阅读(1991)  评论(2编辑  收藏  举报