还是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[] {1, 2, 3, 4});
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[] {1, 2, 3, 4});
var answerGenerator = mockAnswerGenerator.Object;
game = new Game(answerGenerator, mockHistory.Object);
game.Guess(new int[] {5, 6, 7, 8});
mockHistory.Verify(h => h.Add(new int[] {5, 6, 7, 8}, "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
public class Game
{
private const string CorrectGuess = "4A0B";
private const int MaxGuessTimes = 6;
private readonly int[] answer;
private readonly IGameHistory gameHistory;
private int guessTimes;
public Game(IAnswerGenerator answerGenerator)
{
answer = answerGenerator.Generate();
}
public Game(IAnswerGenerator answerGenerator, IGameHistory gameHistory) : this(answerGenerator)
{
this.gameHistory = gameHistory;
}
public delegate void GameEventHandler();
public event GameEventHandler GameOver;
public event GameEventHandler GameClear;
public string Guess(int[] guess)
{
guessTimes++;
string result = GetGuessResult(guess);
RecordGuess(guess, result);
if (IsGameClear(result))
{
OnGameClear();
}
else if (CanContinueGuess())
{
OnGameOver();
}
return result;
}
private void RecordGuess(int[] guess, string result)
{
if (gameHistory != null)
{
gameHistory.Add(guess, result);
}
}
private bool IsGameClear(string result)
{
return result == CorrectGuess;
}
private bool CanContinueGuess()
{
return guessTimes >= MaxGuessTimes;
}
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 OnGameClear()
{
if (GameClear != null)
{
GameClear();
}
}
protected void OnGameOver()
{
if (GameOver != null)
{
GameOver();
}
}
} 测试代码
Code
[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();
void GameClear();
}
[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_record_guess_history()
{
var mockHistory = new Mock<IGameHistory>();
var mockAnswerGenerator = new Mock<IAnswerGenerator>();
mockAnswerGenerator.Setup(generator => generator.Generate()).Returns(new[] {1, 2, 3, 4});
var answerGenerator = mockAnswerGenerator.Object;
game = new Game(answerGenerator, mockHistory.Object);
game.Guess(new[] {5, 6, 7, 8});
mockHistory.Verify(h => h.Add(new[] {5, 6, 7, 8}, "0A0B"));
}
[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"));
}
[Test]
public void should_win_when_guess_correct()
{
var mockObserver = new Mock<IGameObserver>();
game.GameClear += mockObserver.Object.GameClear;
game.Guess(new[] {1, 2, 3, 4});
mockObserver.Verify(m => m.GameClear(), Times.Exactly(1));
}
} 更新to-do list
随机生成答案
检查输入是否合法
判断猜测结果
记录历史猜测数据并显示
判断猜测次数,如果满6次但是未猜对则判负
如果4个数字全中,则判胜
实现IRandomIntGenerator
实现IGameHistory
显示猜测纪录
我们完成了记录历史数据的任务,但是显示的任务并未完成,同时我们还没有对IGameHistory进行实现, 因此又添加了两个任务
相关文章
如何开始TDD
测试驱动开发之可执行文档
三张卡片帮你记住TDD原则