大家好:
今天的TDD练习又开始了。回头看看上一次留下的任务。
To-Do-List:
新建ValidationTest文件。
分析需求:(1)不重复。(2)4位(3)数字。(4)不为空。
按照我们分析出来的4个明确点我们开始写CASE。
注意命名!
- [TestClass]
- publicclassValidatorTest
- {
- privateValidatorvalidator;
- [TestInitialize]
- publicvoidInit()
- {
- validator=newValidator();
- }
- [TestMethod]
- publicvoidshould_return_input_must_be_four_digits_when_input_figures_digit_is_not_four_digits()
- {
- varinput="29546";
- validator.Validate(input);
- varactual=validator.ErrorMsg;
- Assert.AreEqual("theinputmustbefourdigits.",actual);
- }
- [TestMethod]
- publicvoidshould_return_input_must_be_fully_digital_when_input_is_not_all_digital()
- {
- varinput="a4s5";
- validator.Validate(input);
- varactual=validator.ErrorMsg;
- Assert.AreEqual("theinputmustbefullydigital.",actual);
- }
- [TestMethod]
- publicvoidshould_return_input_can_not_be_empty_when_input_is_empty()
- {
- varinput="";
- validator.Validate(input);
- varactual=validator.ErrorMsg;
- Assert.AreEqual("theinputcan'tbeempty.",actual);
- }
- [TestMethod]
- publicvoidshould_return_input_can_not_contain_duplicate_when_input_figures_contain_duplicate()
- {
- varinput="2259";
- validator.Validate(input);
- varactual=validator.ErrorMsg;
- Assert.AreEqual("theinputfigurescan'tcontainduplicate.",actual);
- }
- }
- publicclassValidator
- {
- publicstringErrorMsg{get;privateset;}
- publicboolValidate(stringinput)
- {
- if(string.IsNullOrEmpty(input))
- {
- ErrorMsg="theinputcan'tbeempty.";
- returnfalse;
- }
- if(input.Length!=4)
- {
- ErrorMsg="theinputmustbefourdigits.";
- returnfalse;
- }
- varregex=newRegex(@"^[0-9]*$");
- if(!regex.IsMatch(input))
- {
- ErrorMsg="theinputmustbefullydigital.";
- returnfalse;
- }
- if(input.Distinct().Count()!=4)
- {
- ErrorMsg="theinputfigurescan'tcontainduplicate.";
- returnfalse;
- }
- returntrue;
- }
- }
Run...
一个CASE对应这一个IF。也可合并2个CASE。可以用"^\d{4}$"去Cover"4位数字"。可以根据自己的情况去定。
小步前进不一定要用很小粒度去一步一步走。这样开发起来的速度可能很慢。依靠你自身的情况去决定这一小步到底应该有多大。正所谓"步子大了容易扯到蛋,步子小了前进太慢"。只要找到最合适自己的步子。才会走的更好。
这么多IF看起来很蛋疼。有测试。可以放心大胆的重构。把每个IF抽出一个方法。看起来要清晰一些。
- publicclassValidator
- {
- publicstringErrorMsg{get;privateset;}
- publicboolValidate(stringinput)
- {
- returnIsEmpty(input)&&IsFourdigits(input)&&IsDigital(input)&&IsRepeat(input);
- }
- privateboolIsEmpty(stringinput)
- {
- if(!string.IsNullOrEmpty(input))
- {
- returntrue;
- }
- ErrorMsg="theinputcan'tbeempty.";
- returnfalse;
- }
- privateboolIsFourdigits(stringinput)
- {
- if(input.Length==4)
- {
- returntrue;
- }
- ErrorMsg="theinputmustbefourdigits.";
- returnfalse;
- }
- privateboolIsDigital(stringinput)
- {
- varregex=newRegex(@"^[0-9]*$");
- if(regex.IsMatch(input))
- {
- returntrue;
- }
- ErrorMsg="theinputmustbefullydigital.";
- returnfalse;
- }
- privateboolIsRepeat(stringinput)
- {
- if(input.Distinct().Count()==4)
- {
- returntrue;
- }
- ErrorMsg="theinputfigurescan'tcontainduplicate.";
- returnfalse;
- }
- }
为了确保重构正确。重构之后一定要把所有的CASE在跑一遍,确定所有的都PASS。
To-Do-List:
猜测数字
输入验证
生成答案
输入次数
输出猜测结果
验证搞定了。我们来整整随机数。
分析需求:产品代码需要一个随机生成的答案。(1)不重复。(2)4位(3)数字。
这里有个问题:大家都知道随机数是个概率的问题。因为每次生成的数字都不一样。看看之前Guesser类的代码。
- publicclassGuesser
- {
- privateconststringAnswerNumber="2975";
- publicstringGuess(stringinputNumber)
- {
- varaCount=0;
- varbCount=0;
- for(varindex=0;index<AnswerNumber.Length;index++)
- {
- if(AnswerNumber[index]==inputNumber[index])
- {
- aCount++;
- continue;
- }
- if(AnswerNumber.Contains(inputNumber[index].ToString()))
- {
- bCount++;
- }
- }
- returnstring.Format("{0}a{1}b",aCount,bCount);
- }
- }
这里我们如果把private const string AnswerNumber = "2975";改为随机的话,那Guesser类测试的结果是不能确定的。也就是说测试依赖了一些可变的东西。比如:随机数、时间等等。
遇到这种情况应该怎么办呢?一种随机数是给产品代码用,我们可以MOCK另外一种"固定随机数"(但是要满足生成随机数的条件)来给测试用。
还是一样先写测试。
- [TestClass]
- publicclassAnswerGeneratorTest
- {
- [TestMethod]
- publicvoidshould_pass_when_answer_generator_number_is_four_digits_and_fully_digital()
- {
- Regexregex=newRegex(@"^\d{4}$");
- varanswerGenerator=newAnswerGenerator();
- varactual=regex.IsMatch(answerGenerator.Generate());
- Assert.AreEqual(true,actual);
- }
- [TestMethod]
- publicvoidshould_pass_when_answer_generator_number_do_not_repeat()
- {
- varanswerGenerator=newAnswerGenerator();
- varactual=answerGenerator.Generate().Distinct().Count()==4;
- Assert.AreEqual(true,actual);
- }
- }
实现AnswerGenerator类让测试通过。
- publicclassAnswerGenerator
- {
- publicstringGenerate()
- {
- varanswerNumber=newStringBuilder();
- 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()));
- returnanswerNumber.ToString();
- }
- }
运行测试。
为了解决测试依赖可变的问题。定义IAnswerGenerator。让两种随机数类继承。
- publicinterfaceIAnswerGenerator
- {
- stringGenerate();
- }
- publicclassAnswerGenerator:IAnswerGenerator
- {
- publicstringGenerate()
- {
- varanswerNumber=newStringBuilder();
- Enumerable.Range(0,k=Guid.NewGuid().ToString()})
- .OrderBy(x=>x.k)
- .Select(x=>x.v)
- .Take(4).ToList()
- .ForEach(num=>answerNumber.Append(num.ToString()));
- returnanswerNumber.ToString();
- }
- }
- publicclassAnswerGeneratorForTest:IAnswerGenerator
- {
- publicstringGenerate()
- {
- return"2975";
- }
- }
AnswerGenerator给产品代码用。AnswerGeneratorForTest给测试代码用。这样就可以避免测试依赖可变的问题。
- publicclassGuesser
- {
- publicstringAnswerNumber{get;privateset;}
- publicGuesser(IAnswerGeneratorgenerator)
- {
- AnswerNumber=generator.Generate();
- }
- publicstringGuess(stringinputNumber)
- {
- ...
- }
- }
- [TestClass]
- publicclassGuesserTest
- {
- privateGuesserguesser;
- [TestInitialize]
- publicvoidInit()
- {
- guesser=newGuesser(newAnswerGeneratorForTest());
- }
- ...
- }
这样我在测试代码当中就会给一个不可变的随机数。AnswerGeneratorForTest。所以之前的Guesser测试代码也不会因为每次的随机数不一样导致挂掉。
产品代码呢?直接丢AnswerGenerator进去就妥妥地。
To-Do-List:
猜测数字
输入验证
生成答案
输入次数
输出猜测结果
OK。今天的收获。
(1)小步前进:依靠自身情况决定“小步”应该有多大。
(2)重构:之前的测试是我们重构的保障。
(3)测试依赖:测试不应该依赖于一些可变的东西。
都到这了,有没有点TDD的感觉。知道TDD的步骤了吗?
(1)新增一个测试。
(2)运行所有的测试程序并失败。
(3)做一些小小的改动。
(4)运行所有的测试,并且全部通过。
(5)重构代码以消除重复设计,优化设计。
(6)重复上面的工作。实现1~5小范围迭代。直到满足今天的Story。
上一篇还有个遗留的问题。我把它记在小本上。
(完)
原文链接:https://www.f2er.com/javaschema/285822.html