单元测试中三种准备Test Fixture的方法比较

首先说一下Test Fixture,我不知道怎么样翻译这个Test Fixture,没能搜到一个翻译的比较合适的。最让我气愤的是某人翻译的一本书中,直接把Test Fixture翻译成为测试夹具,这明显就是什么词霸词典硬翻译出来的,我强烈鄙视这样不负责任的翻译行为。

The test fixture is everything we need to have in place to exercise the SUT

我觉得这是一个对Test Fixture的一个很清晰明了的定义,就是运行被测软件所需要的一切东西,这个“东西”不单只是数据,同时还包括对被测软件的准备,例如实例化某个被测方法所在的类,准备数据库的ConnectionString等。通常来说,有三种方法来准备Test Fixture。

1. 内联方式:这种方式就是直接在测试方法中编写准备Test Fixture的代码。用这种方法的缺点是很容易造成代码的重复,出现很多复制粘贴的代码。同时,如果这个SETUP的过程比较复杂,也会降低测试代码的可读性,可维护性。另外的一个问题就是,这种方法很容易会带来测试数据Hard code的隐患。既然有那么多缺点,这种方法还有什么生命力呢?首先,可能对于初学者来说,这种方法是最简单的;其次,在一些只需要准备简单的Test Fixture的场合中,这种方法还是给编写测试的人提供了便利。

2. 委托方式:简单来说就是把Test Fixture的准备抽取为一个外部的方法,然后在需要的时候进行调用。这种方式的好处就是使得测试代码可读性更强,并且这部分的SETUP代码可以重用。而且这种做法可以屏蔽对SETUP过程的认知,使得测试人员的关注点落在真正的测试代码上面,而不是如何SETUP。

3. 隐式方式:很多xUnit框架的实现都提供了不同的隐式SETUP和TEARDOWN。例如MSTEST里面的[TestInitialize]和[TestCleanup]标签,就提供了一种隐式准备Test Fixture的支持。就是在每一个测试方法运行前,都会执行一次标有[TestInitialize]标签的方法。使用这种方法的好处就是写一次就能在各个测试中都实现了Test Fixture的准备,不用每次都显示地调用一个外部方法,不过缺点也不少:

  • 可能会令测试比较难懂,因为这些隐式调用不是必须的,有可能会被遗漏掉。
  • 不能使用哦本地变量来保存对象,只能用test class 里面的filed或者property
  • 变相地使用了全局变量

在做单元测试的过程中,需要灵活地运用这三种Test Fixture的准备方法。例如在我的工作当中,我在以下情况使用到了隐式准备方式:

在某一个测试项目中,由于测试的数据不多,所以我使用了XmlSerializer,把测试的数据都放在一个XML文件中,然后利用XmlSerializer把XML中的数据映射到相应的配置类中。对于这样的情况,这些测试数据在测试运行的全过程中都需要用到,所以我选择了在测试类初始化的时候,只运行1次,来准备这些测试数据(Test Fixture的一部分)。

[ClassInitialize()]
public static void MyClassInitialize(TestContext testContext)
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(Config));
    FileStream fs = new FileStream("Config.xml", FileMode.Open);
    config = (Config)xmlSerializer.Deserialize(fs);
}

还是在同一个项目中,我想让每一个测试之间互相不要受到影响,所以我在[TestInitialize()]方法中对于一个Web Service的客户端进行初始化,保证每一次运行测试前,这个客户端都是“新”的

[TestInitialize()]
public void MyTestInitialize()
{
   USi18nService = new InteropWebSerivce();
   USi18nService.Url = config.USi18nAddress;
}

在另外的一个项目中,需要对一个方法进行测试,该方法的功能是根据Email地址从PreSignup表里面获取相关的资料,那么首先要做的就是完成一个PreSignup的操作,才能检查这个方法。而完成PreSignup操作并不是所有的测试都需要的,所以我不选择隐式调用,而是选择委托调用,调用一个外部的帮助方法,来帮助我完成PreSignup操作;同时这个PreSignup的操作在其他一些测试中也是需要的,所以也达到了重用的效果。

那么在什么时候会用到内联方法来准备Test Fixture呢?其实我自己经常用这种方法,有以下一个场景,我需要对一个方法进行测试,这个方法要做的事情就是检查一个电话号码是否符合规范,那么我就会先创建一个List,然后在List里面填充了各种不同的电话号码,然后在后面用一个foreach语句把List里面的数据遍历一遍,可能这些电话号码的数据只在这一个方法里面才有用,所以我没有选择把它抽取成一个方法。

其实不同的单元测试框架有不同的思想,例如在NUnit里面,一个测试类的标签就是叫[Test Fixture],其实作者的设计思想就是一个测试类,就是一套Test Fixture;如果不是一套Test Fixture,那么不要把测试方法写到一起。其实这三种方法各有所长,在我刚开始学习和尝试做单元测试的时候,我刚接触到类似[Setup][TearDown]这样的隐式调用的时候,我觉得这就是银弹,我要充分使用它。但是随着工作的深入,发现这个隐式调用会带来一些问题,然后就慢慢转用了调外部方法,或者直接内联到测试方法中。只有结合实际情况,结合的上下文来运用这些方法,才能真正提高单元测试代码的质量,减少我们的工作量。

2 thoughts on “单元测试中三种准备Test Fixture的方法比较”

Leave a Reply

Your email address will not be published. Required fields are marked *