其他分享
首页 > 其他分享> > TDD by example (4) -- 变招

TDD by example (4) -- 变招

作者:互联网

原文链接:http://www.cnblogs.com/wangyh/archive/2009/07/13/TDD-by-example-4.html

先看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[] {5, 6, 7, 8};
    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[] {5, 6, 7, 8}), 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方法中的每一行代码都在一个抽象程度上,保持函数短小,易于理解。

完整的测试代码

ContractedBlock.gifExpandedBlockStart.gifCode
[TestFixture]
public class TestGame
{
    private IAnswerGenerator answerGenerator;

    [SetUp]
    public void Setup()
    {
        var mockAnswerGenerator = new Mock<IAnswerGenerator>();
        mockAnswerGenerator.Setup(generator => generator.Generate()).Returns(new[] {1, 2, 3, 4});
        answerGenerator = mockAnswerGenerator.Object;
    }


    [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[] {5, 6, 7, 8};
        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(new Game(answerGenerator, null).Guess(new[] {5, 6, 7, 8}), Is.EqualTo("0A0B"));
    }

    [Test]
    public void should_return_0A2B_when_two_numbers_are_correct_but_positions_are_not_correct()
    {
        Assert.That(new Game(answerGenerator, null).Guess(new[] {3, 4, 5, 6}), Is.EqualTo("0A2B"));
    }

    [Test]
    public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(new Game(answerGenerator, null).Guess(new[] {1, 5, 6, 7}), Is.EqualTo("1A0B"));
    }

    [Test]
    public void should_return_2A2B_when_two_numbers_are_pisition_correct_and_two_are_nunmber_correct()
    {
        Assert.That(new Game(answerGenerator, null).Guess(new[] {1, 2, 4, 3}), Is.EqualTo("2A2B"));
    }

    [Test]
    public void should_return_4A0B_when_all_numbers_are_pisition_correct()
    {
        Assert.That(new Game(answerGenerator, null).Guess(new[] {1, 2, 3, 4}), Is.EqualTo("4A0B"));
    }
}

 

使用委托的方式与接口的方式类似,只是在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[] {5, 6, 7, 8};
    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[] {1, 2, 3, 4});
        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[] {5, 6, 7, 8};
        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[] {5, 6, 7, 8}), Is.EqualTo("0A0B"));
    }

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

    [Test]
    public void should_return_1A0B_when_one_number_is_correct_and_position_is_correct_too()
    {
        Assert.That(game.Guess(new[] {1, 5, 6, 7}), 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[] {1, 2, 4, 3}), Is.EqualTo("2A2B"));
    }

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

 

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

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

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

 

相关文章

如何开始TDD

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

三张卡片帮你记住TDD原则 

 

 

转载于:https://www.cnblogs.com/wangyh/archive/2009/07/13/TDD-by-example-4.html

标签:Guess,int,example,变招,Game,GameOver,new,TDD,public
来源: https://blog.csdn.net/weixin_30722589/article/details/96574258