测试驱动开发TDD(二)

来源:互联网 发布:黒帽seo做adsense 编辑:程序博客网 时间:2024/05/06 14:28

大家好:

  今天的TDD练习又开始了。回头看看上一次留下的任务。

To-Do-List:

猜测数字
输入验证
生成答案
输入次数
输出猜测结果

今天我们把输入验证和随机生成答案搞定。

新建ValidationTest文件。

分析需求:(1)不重复。(2)4位(3)数字。(4)不为空。

按照我们分析出来的4个明确点我们开始写CASE。

注意命名!

[csharp] view plaincopy
  1. [TestClass]  
  2. public class ValidatorTest  
  3. {  
  4.     private Validator validator;  
  5.     [TestInitialize]  
  6.     public void Init()  
  7.     {  
  8.         validator = new Validator();  
  9.     }  
  10.   
  11.     [TestMethod]  
  12.     public void should_return_input_must_be_four_digits_when_input_figures_digit_is_not_four_digits()  
  13.     {  
  14.         var input = "29546";  
  15.         validator.Validate(input);  
  16.         var actual = validator.ErrorMsg;  
  17.         Assert.AreEqual("the input must be four digits.", actual);  
  18.     }  
  19.   
  20.     [TestMethod]  
  21.     public void should_return_input_must_be_fully_digital_when_input_is_not_all_digital()  
  22.     {  
  23.         var input = "a4s5";  
  24.         validator.Validate(input);  
  25.         var actual = validator.ErrorMsg;  
  26.         Assert.AreEqual("the input must be fully digital.", actual);  
  27.     }  
  28.   
  29.     [TestMethod]  
  30.     public void should_return_input_can_not_be_empty_when_input_is_empty()  
  31.     {  
  32.         var input = "";  
  33.         validator.Validate(input);  
  34.         var actual = validator.ErrorMsg;  
  35.         Assert.AreEqual("the input can't be empty.", actual);  
  36.     }  
  37.   
  38.     [TestMethod]  
  39.     public void should_return_input_can_not_contain_duplicate_when_input_figures_contain_duplicate()  
  40.     {  
  41.         var input = "2259";  
  42.         validator.Validate(input);  
  43.         var actual = validator.ErrorMsg;  
  44.         Assert.AreEqual("the input figures can't contain duplicate.", actual);  
  45.     }  
  46. }  
按照第一篇的步骤。实现validator。争取让所有的CASE都过。

[csharp] view plaincopy
  1. public class Validator  
  2.     {  
  3.         public string ErrorMsg { getprivate set; }  
  4.       
  5.         public bool Validate(string input)  
  6.         {  
  7.             if (string.IsNullOrEmpty(input))  
  8.             {  
  9.                 ErrorMsg = "the input can't be empty.";  
  10.                 return false;  
  11.             }  
  12.             if (input.Length != 4)  
  13.             {  
  14.                 ErrorMsg = "the input must be four digits.";  
  15.                 return false;  
  16.             }  
  17.             var regex = new Regex(@"^[0-9]*$");  
  18.             if (!regex.IsMatch(input))  
  19.             {  
  20.                 ErrorMsg = "the input must be fully digital.";  
  21.                 return false;  
  22.             }  
  23.             if (input.Distinct().Count() != 4)  
  24.             {  
  25.                 ErrorMsg = "the input figures can't contain duplicate.";  
  26.                 return false;  
  27.             }  
  28.             return true;  
  29.         }  
  30.     }  

Run...


一个CASE对应这一个IF。也可合并2个CASE。可以用"^\d{4}$"去Cover"4位数字"。可以根据自己的情况去定。

小步前进不一定要用很小粒度去一步一步走。这样开发起来的速度可能很慢。依靠你自身的情况去决定这一小步到底应该有多大。正所谓"步子大了容易扯到蛋,步子小了前进太慢"。只要找到最合适自己的步子。才会走的更好。

 

这么多IF看起来很蛋疼。有测试。可以放心大胆的重构。把每个IF抽出一个方法。看起来要清晰一些。

[csharp] view plaincopy
  1. public class Validator  
  2.     {  
  3.         public string ErrorMsg { getprivate set; }  
  4.   
  5.         public bool Validate(string input)  
  6.         {  
  7.             return IsEmpty(input) && IsFourdigits(input) && IsDigital(input) && IsRepeat(input);  
  8.         }  
  9.   
  10.         private bool IsEmpty(string input)  
  11.         {  
  12.             if (!string.IsNullOrEmpty(input))  
  13.             {  
  14.                 return true;  
  15.             }  
  16.             ErrorMsg = "the input can't be empty.";  
  17.             return false;  
  18.         }  
  19.         private bool IsFourdigits(string input)  
  20.         {  
  21.             if (input.Length == 4)  
  22.             {  
  23.                 return true;  
  24.             }  
  25.             ErrorMsg = "the input must be four digits.";  
  26.             return false;  
  27.         }  
  28.         private bool IsDigital(string input)  
  29.         {  
  30.             var regex = new Regex(@"^[0-9]*$");  
  31.             if (regex.IsMatch(input))  
  32.             {  
  33.                 return true;  
  34.             }  
  35.             ErrorMsg = "the input must be fully digital.";  
  36.             return false;  
  37.         }  
  38.         private bool IsRepeat(string input)  
  39.         {  
  40.             if (input.Distinct().Count() == 4)  
  41.             {  
  42.                 return true;  
  43.             }  
  44.             ErrorMsg = "the input figures can't contain duplicate.";  
  45.             return false;  
  46.         }  
  47.     }  

为了确保重构正确。重构之后一定要把所有的CASE在跑一遍,确定所有的都PASS。

To-Do-List:
猜测数字
输入验证
生成答案
输入次数
输出猜测结果

验证搞定了。我们来整整随机数。

分析需求:产品代码需要一个随机生成的答案。(1)不重复。(2)4位(3)数字。

这里有个问题:大家都知道随机数是个概率的问题。因为每次生成的数字都不一样。看看之前Guesser类的代码。

[csharp] view plaincopy
  1. public class Guesser  
  2.     {  
  3.         private const string AnswerNumber = "2975";  
  4.         public string Guess(string inputNumber)  
  5.         {  
  6.             var aCount = 0;  
  7.             var bCount = 0;  
  8.             for (var index = 0; index < AnswerNumber.Length; index++)  
  9.             {  
  10.                 if (AnswerNumber[index]==inputNumber[index])  
  11.                 {  
  12.                     aCount++;  
  13.                     continue;  
  14.                 }  
  15.                 if (AnswerNumber.Contains(inputNumber[index].ToString()))  
  16.                 {  
  17.                     bCount++;  
  18.                 }  
  19.             }  
  20.             return string.Format("{0}a{1}b", aCount, bCount);  
  21.         }  
  22.     }  

这里我们如果把private const string AnswerNumber = "2975";改为随机的话,那Guesser类测试的结果是不能确定的。也就是说测试依赖了一些可变的东西。比如:随机数、时间等等。

遇到这种情况应该怎么办呢?一种随机数是给产品代码用,我们可以MOCK另外一种"固定随机数"(但是要满足生成随机数的条件)来给测试用。

还是一样先写测试。

[csharp] view plaincopy
  1. [TestClass]  
  2.     public class AnswerGeneratorTest  
  3.     {  
  4.         [TestMethod]  
  5.         public void should_pass_when_answer_generator_number_is_four_digits_and_fully_digital()  
  6.         {  
  7.             Regex regex = new Regex(@"^\d{4}$");  
  8.             var answerGenerator = new AnswerGenerator();  
  9.             var actual = regex.IsMatch(answerGenerator.Generate());  
  10.             Assert.AreEqual(true, actual);  
  11.         }  
  12.   
  13.         [TestMethod]  
  14.         public void should_pass_when_answer_generator_number_do_not_repeat()  
  15.         {  
  16.             var answerGenerator = new AnswerGenerator();  
  17.             var actual = answerGenerator.Generate().Distinct().Count() == 4;  
  18.             Assert.AreEqual(true, actual);  
  19.         }  
  20.     }  

实现AnswerGenerator类让测试通过。

引用cao大一段代码稍加修改

[csharp] view plaincopy
  1. public class AnswerGenerator  
  2.     {  
  3.         public string Generate()  
  4.         {  
  5.             var answerNumber = new StringBuilder();  
  6.             Enumerable.Range(0, 9)  
  7.                 .Select(x => new { v = x, k = Guid.NewGuid().ToString() })  
  8.                 .OrderBy(x => x.k)  
  9.                 .Select(x => x.v)  
  10.                 .Take(4).ToList()  
  11.                 .ForEach(num => answerNumber.Append(num.ToString()));  
  12.             return answerNumber.ToString();  
  13.         }  
  14.     }  

运行测试。


为了解决测试依赖可变的问题。定义IAnswerGenerator。让两种随机数类继承。

[csharp] view plaincopy
  1. public interface IAnswerGenerator  
  2.     {  
  3.         string Generate();  
  4.     }  

[csharp] view plaincopy
  1. public class AnswerGenerator : IAnswerGenerator  
  2.     {  
  3.         public string Generate()  
  4.         {  
  5.             var answerNumber = new StringBuilder();  
  6.             Enumerable.Range(0, 9)  
  7.                 .Select(x => new { v = x, k = Guid.NewGuid().ToString() })  
  8.                 .OrderBy(x => x.k)  
  9.                 .Select(x => x.v)  
  10.                 .Take(4).ToList()  
  11.                 .ForEach(num => answerNumber.Append(num.ToString()));  
  12.             return answerNumber.ToString();  
  13.         }  
  14.     }  

[csharp] view plaincopy
  1. public class AnswerGeneratorForTest : IAnswerGenerator  
  2.     {  
  3.         public string Generate()  
  4.         {  
  5.             return "2975";  
  6.         }  
  7.     }  

AnswerGenerator给产品代码用。AnswerGeneratorForTest给测试代码用。这样就可以避免测试依赖可变的问题。

相应的给Guesser类以及测试代码做个修改。

[csharp] view plaincopy
  1. public class Guesser   
  2.     {  
  3.         public string AnswerNumber { getprivate set; }  
  4.           
  5.         public Guesser(IAnswerGenerator generator)  
  6.         {  
  7.             AnswerNumber = generator.Generate();  
  8.         }  
  9.         public string Guess(string inputNumber)  
  10.         {  
  11.             ...  
  12.         }  
  13.     }  

[csharp] view plaincopy
  1. [TestClass]  
  2. public class GuesserTest  
  3. {  
  4.     private Guesser guesser;  
  5.     [TestInitialize]  
  6.     public void Init()  
  7.     {  
  8.         guesser = new Guesser(new AnswerGeneratorForTest());  
  9.     }  
  10.     ...  
  11. }  

这样我在测试代码当中就会给一个不可变的随机数。AnswerGeneratorForTest。所以之前的Guesser测试代码也不会因为每次的随机数不一样导致挂掉。

产品代码呢?直接丢AnswerGenerator进去就妥妥地。

To-Do-List:
猜测数字
输入验证
生成答案
输入次数
输出猜测结果

OK。今天的收获。

(1)小步前进:依靠自身情况决定“小步”应该有多大。

(2)重构:之前的测试是我们重构的保障。

(3)测试依赖:测试不应该依赖于一些可变的东西。

都到这了,有没有点TDD的感觉。知道TDD的步骤了吗?

(1)新增一个测试。

(2)运行所有的测试程序并失败。

(3)做一些小小的改动。

(4)运行所有的测试,并且全部通过。

(5)重构代码以消除重复设计,优化设计。

(6)重复上面的工作。实现1~5小范围迭代。直到满足今天的Story。

上一篇还有个遗留的问题。我把它记在小本上。

(完)

0 0
原创粉丝点击