TDD by example (4) -- 变招

先看to-do list

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

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

判断猜测次数,如果满6次但是未猜对则判负

我们先理一下思路,如何记录和判断猜测次数和判负。第一种方式是在调用Game类的地方记录和判断,这个类扮演协调者的角色,很可能就是main或类似的东西,他会处理输入输出,调用Game的方法;第二种方式是让Game类自己保存猜测的次数,并在达到6次仍未猜对的情况下出发失败事件;考虑到Game这个类名,判断胜负本来就应该是它的责任,因此这里选择第二种方法。然而这不代表在你的实现里也一定要选这种方式,因为设计本身没有一定的对错,需要综合考虑各种因素,因此我选择第二种方式的原因未必一定也是你选择他的原因。

首先,写测试。测什么呢?给定一个答案,猜测6次全错,应该能够得到游戏失败的通知。至于通知的方式我也有两种选择,一种是用接口实现,一种是用委托,这是典型的Observer模式。这里我用接口实现。

定义一个接口

public interface IGameObserver
{
    
void GameOver();
}

然后用mock来模拟接口的实现以便测试

[Test]
public void should_fail_game_when_guess_six_times_and_still_wrong()
{
    var mockObserver 
= new Mock<IGameObserver>();
    var game 
= new Game(answerGenerator, mockObserver.Object);
    var wrongGuess 
= new[] {5678};
    
for (int i = 0; i < 5; i++)
    {
        game.Guess(wrongGuess);
    }
    mockObserver.Verify(m 
=> m.GameOver(), Times.Never());
    game.Guess(wrongGuess);
    mockObserver.Verify(m 
=> m.GameOver(), Times.Exactly(1));
}

在这里,我首先mock了一个IGameObserver的对象,然后传给Game类,接下来作5次错误的猜测,验证GameOver并没有被调用,然后猜测第6次,验证GameOver被调用。需要注意的是,我修改了Game构造函数的接口,添加了一个参数,因此前面我们在TestGame类中写的所有测试,都要在创建Game实例时多穿一个null。如:

[Test]
public void should_return_0A0B_when_no_number_is_correct()
{
    Assert.That(
new Game(answerGenerator, null).Guess(new[] {5678}), Is.EqualTo("0A0B"));
}

Game的构造函数现在变成

public Game(IAnswerGenerator answerGenerator, IGameObserver observer)
{
    
this.answer = answerGenerator.Generate();
}

好了,编译通过,运行测试,以前的测试都通过,这个测试失败,说明我们没有破坏以前的东西,接下来在Game类中添加代码

public class Game
{
    
private readonly IGameObserver observer;
    
private readonly int[] answer;
    
private int guessTimes = 0;

    
public Game(IAnswerGenerator answerGenerator, IGameObserver observer)
    {
        
this.observer = observer;
        
this.answer = answerGenerator.Generate();
    }

    
public string Guess(int[] guess)
    {
        guessTimes
++;
        
int aCount = 0;
        
int bCount = 0;
        
for (int i = 0; i < guess.Length; i++)
        {
            
if (answer[i] == guess[i])
            {
                aCount
++;
            }
            
else if (answer.Contains(guess[i]))
            {
                bCount
++;
            }
        }
        
string result = string.Format("{0}A{1}B", aCount, bCount);
        
if(guessTimes >= 6 && result != "4A0B")
        {
            
if(observer!=null)
            {
                observer.GameOver();
            }
        }
        
return result;
    }
}

这里我们添加了一个记录猜测次数的字段,并在每次猜测的时候累加,如果猜测的次数等于或超过6次,而且猜测的结果还不对的话,就会调用GameOver。好的,运行测试,所有测试都通过,看来我们已经完成了这个功能,而且没有破坏以前实现的功能。接下来是重构时间,看看代码那里可以改进。这里“6”是一个magic number,“4A4B”也是一个,对observer的调用可以抽取一个方法出来……代码如下

public class Game
{
    
private readonly IGameObserver observer;
    
private readonly int[] answer;
    
private int guessTimes = 0;
    
const string CorrectGuess = "4A0B";
    
const int MaxGuessTimes = 6;

    
public Game(IAnswerGenerator answerGenerator, IGameObserver observer)
    {
        
this.observer = observer;
        
this.answer = answerGenerator.Generate();
    }

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

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

    
private string GetGuessResult(int[] guess)
    {
        
int aCount = 0;
        
int bCount = 0;
        
for (int i = 0; i < guess.Length; i++)
        {
            
if (answer[i] == guess[i])
            {
                aCount
++;
            }
            
else if (answer.Contains(guess[i]))
            {
                bCount
++;
            }
        }
        
return string.Format("{0}A{1}B", aCount, bCount);
    }

    
protected void OnGameOver()
    {
        
if(observer!=null)
        {
            observer.GameOver();
        }
    }
}

在这里我提出了一个方法GetGuessResult,代码就是之前Guess方法的代码,这样做的目的是使Guess方法中的每一行代码都在一个抽象程度上,保持函数短小,易于理解。

完整的测试代码

Code

 

使用委托的方式与接口的方式类似,只是在mock的设置上略有不同

首先,测试需要做些修改

private interface IGameObserver
{
    
void GameOver();
}
[Test]
public void should_fail_game_when_guess_six_times_and_still_wrong()
{
    var mockObserver 
= new Mock<IGameObserver>();
    var game 
= new Game(answerGenerator);
    game.GameOver 
+= mockObserver.Object.GameOver;
    var wrongGuess 
= new[] {5678};
    
for (int i = 0; i < 5; i++)
    {
        game.Guess(wrongGuess);
    }
    mockObserver.Verify(m 
=> m.GameOver(), Times.Never());
    game.Guess(wrongGuess);
    mockObserver.Verify(m 
=> m.GameOver(), Times.Exactly(1));
}

这里,我们不需要再修改Game类的构造函数了,IGameObserver也不再是实现的一部分,而是一个仅用于测试的接口,测试逻辑还是一样的

Game类也需要修改,需要声明GameOver事件,并在Guess中调用

public class Game
{
    
private readonly int[] answer;
    
private int guessTimes = 0;
    
const string CorrectGuess = "4A0B";
    
const int MaxGuessTimes = 6;

    
public delegate void GameEventHandler();
    
public event GameEventHandler GameOver;
    
    
public Game(IAnswerGenerator answerGenerator)
    {
        
this.answer = answerGenerator.Generate();
    }

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

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

    
private string GetGuessResult(int[] guess)
    {
        
int aCount = 0;
        
int bCount = 0;
        
for (int i = 0; i < guess.Length; i++)
        {
            
if (answer[i] == guess[i])
            {
                aCount
++;
            }
            
else if (answer.Contains(guess[i]))
            {
                bCount
++;
            }
        }
        
return string.Format("{0}A{1}B", aCount, bCount);
    }

    
protected void OnGameOver()
    {
        
if(GameOver!=null)
        {
            GameOver();
        }
    }
}

测试代码也可以重构一下了,将Game的创建提到Setup中

[TestFixture]
public class TestGame
{
    
private Game game;

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

    
public interface IGameObserver
    {
        
void GameOver();
    }
    [Test]
    
public void should_fail_game_when_guess_six_times_and_still_wrong()
    {
        var mockObserver 
= new Mock<IGameObserver>();
        game.GameOver 
+= mockObserver.Object.GameOver;
        var wrongGuess 
= new[] {5678};
        
for (int i = 0; i < 5; i++)
        {
            game.Guess(wrongGuess);
        }
        mockObserver.Verify(m 
=> m.GameOver(), Times.Never());
        game.Guess(wrongGuess);
        mockObserver.Verify(m 
=> m.GameOver(), Times.Exactly(1));
    }

    [Test]
    
public void should_return_0A0B_when_no_number_is_correct()
    {
        Assert.That(game.Guess(
new[] {5678}), Is.EqualTo("0A0B"));
    }

    [Test]
    
public void should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct()
    {
        Assert.That(game.Guess(
new[] {3456}), Is.EqualTo("0A2B"));
    }

    [Test]
    
public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(game.Guess(
new[] {1567}), Is.EqualTo("1A0B"));
    }

    [Test]
    
public void should_return_2A2B_when_two_numbers_are_pisition_correct_and_two_are_nunmber_correct()
    {
        Assert.That(game.Guess(
new[] {1243}), Is.EqualTo("2A2B"));
    }

    [Test]
    
public void should_return_4A0B_when_all_numbers_are_pisition_correct()
    {
        Assert.That(game.Guess(
new[] {1234}), Is.EqualTo("4A0B"));
    }
}

 

至此,我们又完成了一个任务,更新一下to-do list

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

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

 

相关文章

如何开始TDD

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

三张卡片帮你记住TDD原则 

 

 

posted @ 2009-07-13 10:00  Nick Wang (懒人王)  阅读(1718)  评论(5编辑  收藏  举报