TDD by example (3) -- 虚招
上一篇我们已经完成了一个功能,接下来实现其他功能,首先看一下to-do list:
随机生成答案检查输入是否合法
记录历史猜测数据并显示
判断猜测次数,如果满6次但是未猜对则判负
如果4个数字全中,则判胜
我们挑一个随即生成答案吧,这个功能看起来简单,但是真要TDD也不容易。
随机生成答案
写测试先,这个测试应该测什么呢?测每次生成的答案都是随机的么?不是,.NET已经有内置的Random类可以实现了,微软也测过了,我们就不需要再测了。我们需要测试的是每次生成的答案中,每个数字都是不重复的,而且每个数字都界于0-9。于是写出下面的测试代码:
public void should_generate_correct_answer()
{
AnswerGenerator generator = new AnswerGenerator();
var answer = generator.Generate();
Assert.That(answer.Count, Is.EqualTo(4));
Assert.That(answer, Is.Unique);
Assert.That(answer, Is.All.InRange(0, 9));
}
在测试中,我检查了生成的答案是否有4个数字,每个数字是否在0到9之间,以及是否有重复元素。下面我们就可以开始写实现类了。首先创建 AnswerGenerator类,添加方法Generate,返回一个空的new int[]就可以让编译通过了。
{
return new int[]{};
}
运行测试,失败,意料之中。接下来实现这个方法
{
List<int> list = new List<int>();
Random rand = new Random();
while (list.Count < 4)
{
int num = rand.Next(0, 10);
if (!list.Contains(num))
{
list.Add(num);
}
}
return list.ToArray();
}
Bingo!,测试通过。
到这里似乎我们的功能已经完成了,但是仔细看看代码,这里面其实存在问题。(你能否在不看下面的解释之前就看出问题呢?)
-----------------------------------------------------华丽的分割线-----------------------------------------------------------------------
我们看一下实现代码,在函数Generate中调用了Random类来生成随机数,然后再放到List中。这里每次生成的答案是随机的,因此每一次产生的答案都不一样。假设我的实现代码是错误的,比如我写成了rand.Next(0,11)。那么在某次运行测试的时候,有可能并没有生成错误的答案(10)。在这个时候,测试依然会通过。也就是说测试给出了一种假象,明明代码是有问题的,但是测试却会通过,那么这样的测试能保证代码的质量么?
好的测试必须能够以通过或失败来表明代码正确或错误,一个时而通过时而不通过的测试是无效的测试,而且会增加不必要的麻烦。
既然知道了问题在于,我们无法控制Random每次生成的数字,那么我们改进的方式就是把Random从中提取出去,消除随机数的影响,而且随机数的生成本来就不是AnswerGenerator逻辑的一部分,AnswerGenerator的主要逻辑应该是确保每次生成的答案中没有重复的数字。
首先把Random提取为构造函数的参数,然后我们需要用Mock来模拟Random类,因此需要提取出一个接口,代码变成
{
int Next();
}
public class AnswerGenerator :IAnswerGenerator
{
private IRandomIntGenerator rand;
public AnswerGenerator(IRandomIntGenerator rand)
{
this.rand = rand;
}
public int[] Generate()
{
List<int> list = new List<int>();
while (list.Count < 4)
{
int num = rand.Next();
if (!list.Contains(num))
{
list.Add(num);
}
}
return list.ToArray();
}
}
如此我们就可以用mock来模拟了,这里我们用的mock是Moq
public void should_generate_non_repeatable_numbers()
{
var ints = new[] {1, 2, 3, 3, 4, 5, 6};
int index = 0;
var mock = new Mock<IRandomIntGenerator>();
mock.Setup(g => g.Next()).Returns(() => ints[index]).Callback(() => index++);
AnswerGenerator generator = new AnswerGenerator(mock.Object);
var answer = generator.Generate();
Assert.That(answer, Is.EquivalentTo(new int[] {1, 2, 3, 4}));
mock.Verify(g=>g.Next(), Times.Exactly(5));
}
首先我们创建一个数组作为mock的返回值,然后设置每次调用Next方法时会按顺序返回数组的每一个数字,接下来就是用mock对象来初始化AnswerGenerator,并获取答案,最后检查Next方法确实被正好调用了5次。
至此我们已经完成了检查答案中数字不重复的问题,接下来写一个测试来保证每个数字都在0到9之间
public void should_generate_numbers_between_zero_and_nine()
{
var mock = new Mock<IRandomIntGenerator>();
mock.Setup(g => g.Next()).Returns(10);
AnswerGenerator generator = new AnswerGenerator(mock.Object);
Assert.Throws<RandomNumberOutOfRangeException>(()=>generator.Generate());
}
这里可以考虑如果IRandomIntGenerator返回的是一个不在0到9之间的数字时,直接忽略掉,也可以抛出异常。如何决定完全在于你如何为IRandomIntGenerator.Next划分职责,在这里,我们认为IRandomIntGenerator应该只返回0到9之间的数字,因此如果不在这个范围,属于异常行为,因此抛出异常。为此,还需要创建异常类
修改代码让测试通过
{
List<int> list = new List<int>();
while (list.Count < 4)
{
int num = rand.Next();
if(num<0 || num > 9)
{
throw new RandomNumberOutOfRangeException();
}
if (!list.Contains(num))
{
list.Add(num);
}
}
return list.ToArray();
}
最后再对测试和代码做一些重构,减少重复代码,增加可读性
[TestFixture]
public class TestAnswerGenerator
{
[Test]
public void should_generate_non_repeatable_numbers()
{
var numbers = new[] {1, 2, 3, 3, 4, 5, 6};
int index = 0;
var mock = new Mock<IRandomIntGenerator>();
mock.Setup(g => g.Next()).Returns(() => numbers[index]).Callback(() => index++);
IAnswerGenerator generator = new AnswerGenerator(mock.Object);
var answer = generator.Generate();
Assert.That(answer, Is.EquivalentTo(new int[] {1, 2, 3, 4}));
mock.Verify(g => g.Next(), Times.Exactly(5));
}
[Test]
public void should_generate_numbers_between_zero_and_nine()
{
var mock = new Mock<IRandomIntGenerator>();
mock.Setup(g => g.Next()).Returns(10);
AnswerGenerator generator = new AnswerGenerator(mock.Object);
Assert.Throws<RandomNumberOutOfRangeException>(() => generator.Generate());
}
}
public interface IRandomIntGenerator
{
int Next();
}
public class AnswerGenerator : IAnswerGenerator
{
private const int AnswerLength = 4;
private const int MinNum = 0;
private const int MaxNum = 9;
private readonly IRandomIntGenerator rand;
public AnswerGenerator(IRandomIntGenerator rand)
{
this.rand = rand;
}
public int Gene[]rate()
{
List<int> list = new List<int>();
while (list.Count < AnswerLength)
{
int num = rand.Next();
if (IsInvalid(num))
{
throw new RandomNumberOutOfRangeException();
}
if (!list.Contains(num))
{
list.Add(num);
}
}
return list.ToArray();
}
private static bool IsInvalid(int num)
{
return num < MinNum || num > MaxNum;
}
}
public class RandomNumberOutOfRangeException :Exception
{}
再看看to-do list
随机生成答案检查输入是否合法
记录历史猜测数据并显示
判断猜测次数,如果满6次但是未猜对则判负
如果4个数字全中,则判胜
实现IRandomIntGenerator
我们已经完成了两个任务了,但是由于新加入了一个接口,因此也添加了一个新的任务
相关文章