之前的项目中做单元测试一直用的是NUnit,这次做新项目,负责人要求统一用MsTest,理由是MsTest是Visual Studio内置的。用就用吧,我没什么意见。不过用了两天,我就发现一个大问题:MsTest并不支持参数化测试(也有叫行测试的)。

 

什么是参数化测试?简单的说,就是同样的逻辑,根据输入参数不同给出不同的结果。因为只是参数不同,所以并不希望把测试写多遍,但是又希望对每个参数的测试成为一个独立的测试用例。举例说,假定我有一个数学计算的方法是把数字*2,我希望证明这个方法对于正数、负数和0都是通过的。在NUnit中可以通过TestCaseAttribute来为测试提供参数:

[TestCase(100, 200)]
[TestCase(0, 0)]
[TestCase(-123, -246)]
public void TestDouble(int value, int result)
{
    Assert.AreEqual(result, Calc.Double(value));
}

 

但是MsTest中并没有类似功能的Attribute。怎么办呢?最简单粗暴的方法当然是写一个循环提供参数了。不过这并不是我想要的结果,一是写循环遍历的代码有点无谓;更重要的是循环只能作为一个整体的测试用例来执行,如果出错的话不自己写信息就无法分辨出到底哪个参数出错了。

 

网上找了找,发现问类似问题的人还真不少。基本上回答集中在下面几种方案:

1. VS2012 Update1以后提供了DataRow属性,但是只能在Windows Store Apps使用。(个人觉得这一点好奇葩,一个测试功能还有必要专门优待 Store用户吗?)总之我们做的是普通类库,这个法子不适用。

2. 用DataSourceAttribute,按照文档这个标注可以提供OleDB数据源、XML或者CSV格式的文件作为数据源。对于简单的参数测试这个方案实在有点太重了,再说按照定义使用了外部文件或数据库的单元测试还能算是单元测试吗?

3. 用PostSharp增强代码的方法来扩展测试。该方案我大概看了一下也放弃了,毕竟PostSharp是比较非主流的技术,再说使用它会明显增加编译过程的复杂性,对持续集成也有点不友好。

4. 按照MsTest对象模型来写一些扩展属性。老实说在寻找解决方案之前我已经心里判断应该会走这条路,而且按照网络的示例真的写了几个扩展属性出来测试,编译倒是正常,但测试就是死活运行不起来。再仔细看说明,原来要在Visual Studio运行扩展测试还要修改几个注册表项....好吧我们开发组十几号人都去折腾注册表的话才能测试的话,还让不让小伙伴们开心的一起玩耍了。总之这个方案也Pass。

 

看来看去,就没什么令人满意的办法,唯一还算可行的只有用DataSource了。当然我是不会用数据库这么重量级的数据源的,那对单元测试来说完全是高射炮打蚊子。用文件的话,从存粹的敏捷开发理念来说也是有问题的,但文件毕竟还算相对容易控制。于是我用XML文件测试了一下,通过了,但是有几个小坑需要注意,这里记录一下。

 

代码示例如下,很简单,不解释了:

[TestClass]
[DeploymentItem(@"Fixtures\add.xml")]
public class UnitTest1
{
    public TestContext TestContext { get; set; }

    [TestMethod]
    [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",
        @"|DataDirectory|\Fixtures\add.xml",
        "row",
        DataAccessMethod.Sequential)]
    public void TestMethod1()
    {
        int a = Convert.ToInt32(TestContext.DataRow["a"]);
        int b = Convert.ToInt32(TestContext.DataRow["b"]);
        int expected = Convert.ToInt32(TestContext.DataRow["expected"]);
        var result = new Class1().Add(a, b);
        Assert.AreEqual(expected, result);
    }
}

 

测试用的XML文件:

<rows>
    <row a="1" b="2" expected="3" />
    <row a="2" b="-1" expected="-1" />
    <row a="3" b="0" expected="3" />
</rows>

 

对测试有几点需要注意:

  1. 测试类必须加一个TestContext属性;
  2. XML文件编译属性的 Copy to output directory 应该设置为 Copy always 或者 Copy if newer;
  3. 要在远程服务器上测试成功(比如CI服务器),需要加DeployItem标注,表示这个文件要和程序集一起部署到测试位置;
  4. DataSource的第二个参数里DataDirectory是指什么,MSDN里只拿数据库为例子,没有对XML/CSV的情况做说明。从结果看,应该就是程序输出的位置。

 

结果是可用的,但是老实说,这样的测试代码从观感上来说远不如NUnit那么简单优雅。我觉得微软与其把Visual Studio的测试界面搞得那么复杂,还不如给MsTest增加点实用的核心功能。当然这些是题外话了。