TDD by example (3) -- 虚招

上一篇我们已经完成了一个功能,接下来实现其他功能,首先看一下to-do list:

  随机生成答案
  检查输入是否合法 
  判断猜测结果
  记录历史猜测数据并显示 
  判断猜测次数,如果满6次但是未猜对则判负 

  如果4个数字全中,则判胜 

我们挑一个随即生成答案吧,这个功能看起来简单,但是真要TDD也不容易。

随机生成答案

写测试先,这个测试应该测什么呢?测每次生成的答案都是随机的么?不是,.NET已经有内置的Random类可以实现了,微软也测过了,我们就不需要再测了。我们需要测试的是每次生成的答案中,每个数字都是不重复的,而且每个数字都界于0-9。于是写出下面的测试代码:

[Test]
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(
09));
}

 

在测试中,我检查了生成的答案是否有4个数字,每个数字是否在0到9之间,以及是否有重复元素。下面我们就可以开始写实现类了。首先创建 AnswerGenerator类,添加方法Generate,返回一个空的new int[]就可以让编译通过了。

public int[] Generate() 
{
    
return new int[]{};
}

运行测试,失败,意料之中。接下来实现这个方法

public int[] Generate()
{
    List
<int> list = new List<int>();
    Random rand 
= new Random();
    
while (list.Count < 4)
    {
        
int num = rand.Next(010);
        
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类,因此需要提取出一个接口,代码变成

public interface IRandomIntGenerator
{
    
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

[Test]
public void should_generate_non_repeatable_numbers()
{
    var ints 
= new[] {1233456};
    
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[] {1234}));

    mock.Verify(g
=>g.Next(), Times.Exactly(5));

} 

首先我们创建一个数组作为mock的返回值,然后设置每次调用Next方法时会按顺序返回数组的每一个数字,接下来就是用mock对象来初始化AnswerGenerator,并获取答案,最后检查Next方法确实被正好调用了5次。 

至此我们已经完成了检查答案中数字不重复的问题,接下来写一个测试来保证每个数字都在0到9之间

[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());

} 

这里可以考虑如果IRandomIntGenerator返回的是一个不在0到9之间的数字时,直接忽略掉,也可以抛出异常。如何决定完全在于你如何为IRandomIntGenerator.Next划分职责,在这里,我们认为IRandomIntGenerator应该只返回0到9之间的数字,因此如果不在这个范围,属于异常行为,因此抛出异常。为此,还需要创建异常类

public class RandomNumberOutOfRangeException :Exception{}

 

修改代码让测试通过

public int[] Generate()
{
    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[] {1233456};
        
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[] {1234}));

        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

我们已经完成了两个任务了,但是由于新加入了一个接口,因此也添加了一个新的任务

相关文章

如何开始TDD

测试驱动开发之可执行文档

三张卡片帮你记住TDD原则  

posted @ 2009-07-09 21:24  Nick Wang (懒人王)  阅读(2087)  评论(12编辑  收藏  举报