Unit Test的工作总结(1)

最近屌丝又被他们弄过去做产品的Unit Test(为啥每次啥活都是本屌干?)。其实单元测试本身写起来是比较容易的,只要对软件的结构和逻辑比较熟悉即可。

但问题是要写单元测试的产品是一个维护了十多年的老产品,代码有将近百万行,经过了N次更新换代,代码也被无数的人改过。在这种情况下,在之前开发的过程中根本就没有为单元测试考虑过(使用了大量的异步调用、匿名方法等极难进行测试的东西),再加上产品的结构和逻辑本身就很复杂,所以我也在这两周几乎把能碰的钉子都碰到了。所以要在这里进行记录一下(顺便满足一下某些人的催更……)。


(一)环境的准备

领导希望单元测试能够在每天的build生成后自动跑并生成报表,这是可以理解的,如果我是领导我肯定也希望是这样。我们已经有每天自动生成build的环境,但这个build不包括单元测试,所以需要把这些东西加到每天的编译过程中。但要保证这些产生的DLL不会包含到产品的安装包中,因此可能需要修改安装包或者编译选项。

接下来就可以按照Lab的自动更新来准备单元测试的环境了:在build服务器上找到最近一次的build→拷贝到指定的地方→调用mstest运行测试→分析结果→把结果记录到数据库并发邮件给领导(这一步非常重要 ^_^)。

这个过程没有什么好说的,但看似简单其实很复杂,因为我们考虑到的都是针对正常情况设计的流程,在某些时候还是会遇到很多问题,比如今天的build没出怎么办、失败了怎么办、网络不同怎么办等等,这些问题都是在我管理lab的时候经常遇到的,所以这个自动更新和测试的工具也都是有所考虑的。

在此要说明的一点就是调用mstest生成的结果文件扩展名是trx,可以直接在Visual Studio中打开可以直接看到结果,但是这样不能满足领导的要求(领导想要的是每天发报表),所以我们要自己写程序分析这个文件并生成报表。

不过幸运的是虽然这是一个trx文件,但实际上是一个xml,所以直接使用.NET中提供的XDocument就能把它解析出来。解析xml非常头疼的一点就是命名空间的问题,按节点名字查找的时候如果命名空间不对是找不出来的,这点具体请参考MSDN(而且个人认为微软的XML相关的类设计得很不好,非常难用,有时候感觉到很莫名其妙)。

报表文件不会很大,所以我们就偷懒吧,直接用LINQ遍历所有的节点算了,这也倒是很方便,性能神马的不用考虑,反正每天只跑一次,而且几乎不需要什么时间:

Dim results As IEnumerable(Of XElement) = From el In x.Descendants()
                                                  Select el Where el.Name.LocalName.Equals("UnitTestResult")

写数据发邮件什么的就不多说了,没什么特别的地方。


(二)替换掉某些类的实例

这应该是单元测试经常需要使用的:比如我们在编写客户端的单元测试时我们不想去调用后台服务读系统配置或取数据,也不想与其他系统交互。编写代码的时候考虑到了这一点的话,那么这些都是可以被替换的。

如果产品的类本身就是从工厂中产生的话,那么只需要使用工厂产生替换的对象即可(或者修改一下工厂方法)。如果是使用了Unity管理具体实现的话那更加方便。首先我们找到将具体实现注册到接口的地方:

比如我们想把读配置的接口采用测试时的一个“假”的类,那么自己去注册即可:

container.RegisterInstance<IConfigurationAgent>(new DummyConfigurationAgent());

这样,在程序中所有IConfigurationAgent的具体实现就变成了我们这个DummyConfigurationAgent类了。


(三)使用Shim

但是事情往往没有那么简单!

首先我们并不是所有的对象都是通过Unity管理。由于代码实在是太多,所以代码中很容易有不使用接口而直接使用具体类型或对象的时候:

比如这个地方会弹出一个窗口,而单元测试显然不能有窗口。在这种情况下,我们没法使用我们自己的实现去替代掉它。

再或者,我们要测试一个名称为Request的方法,这个方法里面调用了自己的一个内部方法SendToHost,如下:

而这个方法会调用WCF和别的系统通信,这个时候我们就像替换代码中的其中一部分。

从.NET 4.5开始(Visual Studio 2012要升级到Update 2),在Premium和Ultimate版本中M$提供了Shim和Fakes相关的支持。简单来说就是可以把某个类中的部分实现替换成自己实现(包括系统提供的类库比如System.DateTime等类型都可以)。

首先在引用中右击某个程序集,添加对这个程序集的Fakes引用,比如我们要假冒一个Igt.TableManager.Business.s2sInterface,在引用命名空间时,我们要引用Igt.TableManager.Business.s2sInterface.Fakes。

这样做之后,我们发现就有了很多Shim开头的类:

在具体替换时,我们使用一个委托方法即可替换掉原来的实现(注意委托类型会比原来的方法签名多一个类型:第一个参数是被替换掉的类的类型实例):

要注意的是,使用Fakes和Shim必须在一个ShimsContext对象中使用,如上面代码所示的using代码块中。

这样我们又扫清了一个障碍:遇到没有使用工厂或Unity并且必须要进行提到的时候,可以使用这里所说的方法。但要注意的是必须使用.NET 4.5才行。


(四)私有成员的访问

这里要说的最后一点,就是怎么去访问private对象。

虽然微软提供了访问私有成员的支持(就是生成一个后缀为_Accessor的类,比如TableCloseRequester_Accessor),但是经过我的试验,并不推荐大家使用。主要原因是:

  • 这个新的类型_Accessor虽然能访问私有成员,但是类型与原来的完全不同(微软并不是采用继承或接口实现的方式),所以会如果源代码对具体类型进行了引用,就会产生不兼容(类型不匹配)。

微软似乎意识到了这一点,所以从Visual Studio 2012开始,又从默认的菜单中去掉了这个功能。

那怎么办呢?既然原来写的代码不能进行测试,那么就只能改原来的代码了。

当然我们并不推荐把原来的private访问修饰符改成public的,这里使用一个我觉得还行的替代方法:把原来的private的修饰符改成internal(以后也这么做),这样这个变量或方法就可以对内部可见。

当然单元测试的工程并不是“内部”,所以在编译时要指定一个特定的属性:

这样就不会破坏原来的封装了。

但可以看到这不可避免的要修改源代码,所以我们在以后写代码的时候还是要充分考虑到以后测试的便利。至于具体怎么做,等我研究一下,下一篇此系列的博客再写吧。先要去吃饭了(然后下午打下酱油闪人)。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com