TDD by example (4) -- 变招
作者:互联网
先看to-do list
随机生成答案检查输入是否合法
记录历史猜测数据并显示
判断猜测次数,如果满6次但是未猜对则判负
如果4个数字全中,则判胜
实现IRandomIntGenerator
判断猜测次数,如果满6次但是未猜对则判负
我们先理一下思路,如何记录和判断猜测次数和判负。第一种方式是在调用Game类的地方记录和判断,这个类扮演协调者的角色,很可能就是main或类似的东西,他会处理输入输出,调用Game的方法;第二种方式是让Game类自己保存猜测的次数,并在达到6次仍未猜对的情况下出发失败事件;考虑到Game这个类名,判断胜负本来就应该是它的责任,因此这里选择第二种方法。然而这不代表在你的实现里也一定要选这种方式,因为设计本身没有一定的对错,需要综合考虑各种因素,因此我选择第二种方式的原因未必一定也是你选择他的原因。
首先,写测试。测什么呢?给定一个答案,猜测6次全错,应该能够得到游戏失败的通知。至于通知的方式我也有两种选择,一种是用接口实现,一种是用委托,这是典型的Observer模式。这里我用接口实现。
定义一个接口
{
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类中添加代码
{
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的调用可以抽取一个方法出来……代码如下
{
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[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中
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
随机生成答案检查输入是否合法
记录历史猜测数据并显示
如果4个数字全中,则判胜
实现IRandomIntGenerator
相关文章
转载于: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