測試驅動開發TDD(2)
今天的TDD練習又開始了。回頭看看上一次留下的任務。
To-Do-List:
猜測數字
輸入驗證
生成答案
輸入次數
輸出猜測結果
今天我們把輸入驗證和隨機生成答案搞定。
新建ValidationTest文件。
分析需求:(1)不重復。(2)4位(3)數字。(4)不為空。
按照我們分析出來的4個明確點我們開始寫CASE。
注意命名!
[TestClass] public class ValidatorTest { private Validator validator; [TestInitialize] public void Init() { validator = new Validator(); } [TestMethod] public void should_return_input_must_be_four_digits_when_input_figures_digit_is_not_four_digits() { var input = "29546"; validator.Validate(input); var actual = validator.ErrorMsg; Assert.AreEqual("the input must be four digits.", actual); } [TestMethod] public void should_return_input_must_be_fully_digital_when_input_is_not_all_digital() { var input = "a4s5"; validator.Validate(input); var actual = validator.ErrorMsg; Assert.AreEqual("the input must be fully digital.", actual); } [TestMethod] public void should_return_input_can_not_be_empty_when_input_is_empty() { var input = ""; validator.Validate(input); var actual = validator.ErrorMsg; Assert.AreEqual("the input can't be empty.", actual); } [TestMethod] public void should_return_input_can_not_contain_duplicate_when_input_figures_contain_duplicate() { var input = "2259"; validator.Validate(input); var actual = validator.ErrorMsg; Assert.AreEqual("the input figures can't contain duplicate.", actual); } } |
按照第一篇的步驟。實現validator。爭取讓所有的CASE都過。
public class Validator { public string ErrorMsg { get; private set; } public bool Validate(string input) { if (string.IsNullOrEmpty(input)) { ErrorMsg = "the input can't be empty."; return false; } if (input.Length != 4) { ErrorMsg = "the input must be four digits."; return false; } var regex = new Regex(@"^[0-9]*$"); if (!regex.IsMatch(input)) { ErrorMsg = "the input must be fully digital."; return false; } if (input.Distinct().Count() != 4) { ErrorMsg = "the input figures can't contain duplicate."; return false; } return true; } } |
Run...
一個CASE對應這一個IF。也可合并2個CASE。可以用"^\d{4}$"去Cover"4位數字"。可以根據自己的情況去定。
小步前進不一定要用很小粒度去一步一步走。這樣開發起來的速度可能很慢。依靠你自身的情況去決定這一小步到底應該有多大。正所謂"步子大了容易扯到蛋,步子小了前進太慢"。只要找到最合適自己的步子。才會走的更好。
這么多IF看起來很蛋疼。有測試。可以放心大膽的重構。把每個IF抽出一個方法。看起來要清晰一些。
public class Validator { public string ErrorMsg { get; private set; } public bool Validate(string input) { return IsEmpty(input) && IsFourdigits(input) && IsDigital(input) && IsRepeat(input); } private bool IsEmpty(string input) { if (!string.IsNullOrEmpty(input)) { return true; } ErrorMsg = "the input can't be empty."; return false; } private bool IsFourdigits(string input) { if (input.Length == 4) { return true; } ErrorMsg = "the input must be four digits."; return false; } private bool IsDigital(string input) { var regex = new Regex(@"^[0-9]*$"); if (regex.IsMatch(input)) { return true; } ErrorMsg = "the input must be fully digital."; return false; } private bool IsRepeat(string input) { if (input.Distinct().Count() == 4) { return true; } ErrorMsg = "the input figures can't contain duplicate."; return false; } } |
為了確保重構正確。重構之后一定要把所有的CASE在跑一遍,確定所有的都PASS。
To-Do-List:
猜測數字
輸入驗證
生成答案
輸入次數
輸出猜測結果
驗證搞定了。我們來整整隨機數。
分析需求:產品代碼需要一個隨機生成的答案。(1)不重復。(2)4位(3)數字。
這里有個問題:大家都知道隨機數是個概率的問題。因為每次生成的數字都不一樣。看看之前Guesser類的代碼。
public class Guesser { private const string AnswerNumber = "2975"; public string Guess(string inputNumber) { var aCount = 0; var bCount = 0; for (var index = 0; index < AnswerNumber.Length; index++) { if (AnswerNumber[index]==inputNumber[index]) { aCount++; continue; } if (AnswerNumber.Contains(inputNumber[index].ToString())) { bCount++; } } return string.Format("{0}a{1}b", aCount, bCount); } } |
這里我們如果把private const string AnswerNumber = "2975";改為隨機的話,那Guesser類測試的結果是不能確定的。也就是說測試依賴了一些可變的東西。比如:隨機數、時間等等。
遇到這種情況應該怎么辦呢?一種隨機數是給產品代碼用,我們可以MOCK另外一種"固定隨機數"(但是要滿足生成隨機數的條件)來給測試用。
還是一樣先寫測試。
[TestClass] public class AnswerGeneratorTest { [TestMethod] public void should_pass_when_answer_generator_number_is_four_digits_and_fully_digital() { Regex regex = new Regex(@"^\d{4}$"); var answerGenerator = new AnswerGenerator(); var actual = regex.IsMatch(answerGenerator.Generate()); Assert.AreEqual(true, actual); } [TestMethod] public void should_pass_when_answer_generator_number_do_not_repeat() { var answerGenerator = new AnswerGenerator(); var actual = answerGenerator.Generate().Distinct().Count() == 4; Assert.AreEqual(true, actual); } } |
實現AnswerGenerator類讓測試通過。
引用cao大一段代碼稍加修改
public class AnswerGenerator { public string Generate() { var answerNumber = new StringBuilder(); Enumerable.Range(0, 9) .Select(x => new { v = x, k = Guid.NewGuid().ToString() }) .OrderBy(x => x.k) .Select(x => x.v) .Take(4).ToList() .ForEach(num => answerNumber.Append(num.ToString())); return answerNumber.ToString(); } } |
運行測試。
為了解決測試依賴可變的問題。定義IAnswerGenerator。讓兩種隨機數類繼承。
public interface IAnswerGenerator { string Generate(); } [csharp] view plaincopy public class AnswerGenerator : IAnswerGenerator { public string Generate() { var answerNumber = new StringBuilder(); Enumerable.Range(0, 9) .Select(x => new { v = x, k = Guid.NewGuid().ToString() }) .OrderBy(x => x.k) .Select(x => x.v) .Take(4).ToList() .ForEach(num => answerNumber.Append(num.ToString())); return answerNumber.ToString(); } } public class AnswerGeneratorForTest : IAnswerGenerator { public string Generate() { return "2975"; } } |
AnswerGenerator給產品代碼用。AnswerGeneratorForTest給測試代碼用。這樣就可以避免測試依賴可變的問題。
相應的給Guesser類以及測試代碼做個修改。
public class Guesser { public string AnswerNumber { get; private set; } public Guesser(IAnswerGenerator generator) { AnswerNumber = generator.Generate(); } public string Guess(string inputNumber) { ... } } [TestClass] public class GuesserTest { private Guesser guesser; [TestInitialize] public void Init() { guesser = new Guesser(new AnswerGeneratorForTest()); } ... } |
這樣我在測試代碼當中就會給一個不可變的隨機數。AnswerGeneratorForTest。所以之前的Guesser測試代碼也不會因為每次的隨機數不一樣導致掛掉。
產品代碼呢?直接丟AnswerGenerator進去就妥妥地。
To-Do-List:
猜測數字
輸入驗證
生成答案
輸入次數
輸出猜測結果
OK。今天的收獲。
(1)小步前進:依靠自身情況決定“小步”應該有多大。
(2)重構:之前的測試是我們重構的保障。
(3)測試依賴:測試不應該依賴于一些可變的東西。
都到這了,有沒有點TDD的感覺。知道TDD的步驟了嗎?
(1)新增一個測試。
(2)運行所有的測試程序并失敗。
(3)做一些小小的改動。
(4)運行所有的測試,并且全部通過。
(5)重構代碼以消除重復設計,優化設計。
(6)重復上面的工作。實現1~5小范圍迭代。直到滿足今天的Story。
上一篇還有個遺留的問題。我把它記在小本上。
相關文章:
posted on 2014-01-20 10:09 順其自然EVO 閱讀(211) 評論(0) 編輯 收藏 所屬分類: 測試學習專欄