大家好:
上一篇我剩下的To-Do-List:
今天争取全部搞定。
现在我们Guesser、生成答案、输入验证都有了。把它们组装成一起摇身一变成一个Game!
用一个类把这些职责单一的小模块组合起来。我暂且称它为GameManager.
分析剩下的需求。(1)输入6次GameOver.(2)输入合法数字返回猜测结果。(3)游戏结束提示重新开始游戏。(4)中途输入exit退出游戏。(5)输入正确答案,GameOver。
先把之前写的Guesser提出一个接口。
public interface IGuesser { string AnswerNumber { get; } string Guess(string inputNumber); }
public class Guesser :IGuesser { public string AnswerNumber { get; private set; } public Guesser(IAnswerGenerator generator) { AnswerNumber = generator.Generate(); } public string Guess(string inputNumber) { ... } }
TestFirst.
新建GameManagerTest
[TestClass] public class GameManagerTest { [TestMethod] public void should_return_game_over_when_input_times_is_six_and_result_is_wrong() { IGuesser guess = new Guesser(new AnswerGeneratorForTest()); var game = new GameManager(guess); var input = "1368"; var maxtimes = 6; var actual = false; for (var time = 0; time < maxtimes; time++) { game.Guess(input); } actual = game.IsGameOver; Assert.AreEqual(true,actual); } }
实现GameManager让测试通过。
public class GameManager { private readonly IGuesser guesser; public bool IsGameOver { get; private set; } private const int MAX_TIMES = 6; private int times; public GameManager(IGuesser guesser) { this.guesser = guesser; } public void Guess(string input) { times++; IsGameOver = times == MAX_TIMES; guesser.Guess(input); } }
OK
输入猜测结果。这里包含一个猜对的情况下应该返回"you win"并且Gameover。其他输入返回的结果,在Guesser和validator中已经Cover了。挑几个来测试一下输入输出。
[TestClass] public class GameManagerTest { private _gameManager _game; [TestInitialize] public void Init() { IGuesser guesser = new Guesser(new AnswerGeneratorForTest()); _game = new _gameManager(guesser); } [TestMethod] public void should_return__game_over_when_input_times_is_six_and_result_is_wrong() { const string input = "1368"; const int maxtimes = 6; var actual = false; for (var time = 0; time < maxtimes; time++) { _game.Guess(input); } actual = _game.Is_gameOver; Assert.AreEqual(true,actual); } [TestMethod] public void should_return_you_win_and__game_is_over_when_input_is_equal_answer_number() { const string input = "2975"; var actual = _game.Guess(input); Assert.AreEqual("You win!",actual); Assert.AreEqual(true,_game.Is_gameOver); } [TestMethod] public void should_return_try_again_input_must_be_four_digits_when_input_is_not_equal_four_digits() { const string input = "15243"; var actual = _game.Guess(input); Assert.AreEqual("try again the input must be four digits.",actual); } [TestMethod] public void should_return_try_again_input_can_not_be_empty_when_input_is_empty() { const string input = ""; var actual = _game.Guess(input); Assert.AreEqual("try again the input can't be empty.",actual); } [TestMethod] public void should_return_try_again_input_must_be_fully_digital_when_input_is_not_all_digital() { const string input = "a4sw"; var actual = _game.Guess(input); Assert.AreEqual("try again the input must be fully digital.",actual); } }
修改GameManager类,让所有CASEPASS.
public string Guess(string input) { times++; IsGameOver = times == MAX_TIMES; var validator = new Validator(); if (!validator.Validate(input)) { return "try again " + validator.ErrorMsg; } var result = guesser.Guess(input); if (result == "4a0b") { IsGameOver = true; return "You win!"; } return "try again result is " + result + "."; }
最后:完善GameManager类的work flow。
public class GameManager { private const int MAX_TIMES = 6; private int times; private readonly IGuesser guesser; public bool IsGameOver { get; private set; } public GameManager(IGuesser guesser) { this.guesser = guesser; } private void Start() { times = 0; IsGameOver = false; OutputGameHeader(); } public void Run() { Start(); while (!IsGameOver) { Console.WriteLine(); Console.WriteLine(string.Format("[The {0}st time ] : please input number!",times + 1)); var input = Console.ReadLine(); if (IsExit(input)) continue; var result = Guess(input); Console.WriteLine(result); } OutputGamefooter(); } private bool IsExit(string input) { if (input.ToLower().Trim() == "exit") { Console.WriteLine("Make sure to exit game?(Y/N)"); var readLine = Console.ReadLine(); if (readLine != null) { var isexit = readLine.ToLower().Trim(); if (isexit == "y") { IsGameOver = true; } } return true; } return false; } public string Guess(string input) { times++; IsGameOver = times == MAX_TIMES; var validator = new Validator(); if (!validator.Validate(input)) { return "try again " + validator.ErrorMsg; } var result = guesser.Guess(input); if (result == "4a0b") { IsGameOver = true; return "You win!"; } return "try again result is " + result + "."; } private void OutputGameHeader() { Console.Clear(); Console.WriteLine(" --- Game Start! ---"); Console.WriteLine("---------------------------------------------------------------"); Console.WriteLine("| You can input a number or input exit for exiting this game! |"); Console.WriteLine("---------------------------------------------------------------"); } private void OutputGamefooter() { Console.WriteLine("--------------------------------"); Console.WriteLine("| Game Over! [Answer] is " + guesser.AnswerNumber + " |"); Console.WriteLine("--------------------------------"); } }
Program.cs
class Program { static void Main(string[] args) { var isRepeatGame = false; do { IGuesser guesser = new Guesser(new AnswerGenerator()); var game = new GameManager(guesser); game.Run(); Console.WriteLine("Try again?(Y/N)"); var line = Console.ReadLine(); if (line == null) continue; var readLine = line.ToLower().Trim(); isRepeatGame = readLine == "y"; } while (isRepeatGame); } }
跑下所有的测试。
到这里。这个游戏的基本功能算做完了。做的比较简单。测试和产品代码也比较随意。
大家也可以试着做一做。感受感受测试驱动产品代码。
运行效果
下面是我在实践TDD中遇到的一些问题、以及我个人对它们的理解。
(1)先写测试在写代码开发速度降低了。
开发前期速度确实很慢。当项目越来越大。越来越复杂的时候。改一个bug,或者修改story之后。如何确保产品代码是否正确。手动测试需要多少时间呢?或者调试的时间有多长呢?有了这些测试。可以最大限度的节省你的时间。也许跑一遍测试就可以定位BUG。测试过了。你的修改也就没问题了。
(2)TDD驱动出来的代码。维护性、扩展性如何。
TDD有益于设计。把一个复杂的需求拆分成若干个小模块的过程当中,其实就在思考设计。如何保证每个小模块的职责单一。
(3)后写测试可以不?
我的理解是:第一测试驱动开发是通过测试去驱动产品代码的,如果遇到一个很难的模块(你写不出来的),就可以通过测试一点点的去驱动。第二如果在开发之后写测试的话,问问自己,会写吗?或者是能写全吗?如果有足够的信心。也可以写。
(4)TDD的产物可以方便后期的测试。
试想一下,项目到了后期,在庞大的系统面前,我们要修改某个类、某个方法、修改某个BUG、添加或扩展某个功能的时候。是不感觉特没安全感?会不会为了去找由修改一个小功能而导致其他功能崩溃的原因而抓狂呢?会不会为了定位一个BUG而在各个类之间不断的徘徊呢?会不会感觉到牵一发动全身的感觉呢?软件的坏味等等都会导致这种问题出现。到时候不但被老板骂,还要加班!还要陷入无止尽的各种调试中。(最主要的是你把TEAM里的MM给连累了!)。想避免这种问题吗?想尽快定位BUG的位置吗?如果你想!
说点体会:
(1)清晰的测试方法命名,让我们省去了文档维护的时间。
(2)站在不同角度分析用户需求。拆分Story有益于你的设计(DI)。
(3)所有TDD留下来的测试。可用来做自动化测试,无论你是修BUG,或者添功能。都可以通过自动化测试,快速得到反馈。
(4)有了重构的保证。
(5)进度可视化。可以看出一个复杂的模块,自己完成了多少。(有多少CASE通过)。
(6)小范围迭代。把当前工作重心放在当前这个“小步”上。
需要注意的:
(1)把握好测试的粒度。
(2)测试要尽可能的简单。
(3)测试不要依赖可变。
(4)断言优先。
其实TDD真正有威力的地方是Story划分。以及复杂模块代码的驱动。
以后如果有机会。能理解的更深。会把这两个写出来与大家分享分享。
对这段时间的TDD做个小总结。TDD的范围比较广。而且也比较抽象。以后会加深对TDD的理解。也会把这些记录下来。
代码比较简单。没源码!
大家有时间可以感觉感觉TDD。如果有什么疑问、或者觉得它有什么令你不爽的地方。给我发邮件或者留言http://my.csdn.net/wxr0323
最后 谢zynx蒂姆&YUHENG.
(完)