对数据访问相关代码进行单元测试的探索

来源:互联网 发布:外国人能在淘宝买东西 编辑:程序博客网 时间:2024/05/21 06:32

当我开始学习Unit Test并尝试运用时就遇到一个问题:如何对和数据访问相关的代码进行单元测试。在对数据访问相关代码进行单元测试时,数据库中的内容往往不停地在修改,内容随时间而变化,造成测试时数据库中的数据内容是不确定的。虽然单元测试并没有天天在用,但对这个问题我却一直没有停止探索。我觉得这个问题,已经影响了我对单元测试的应用。

 

那时正好遇到总公司研发部的同事,问他们如何处理这个问题。他们告诉我要使用Mock对象来做。还特地向我解释什么是Mock对象,Mock对象就是像是电影中的替身。当然Mock对象不仅用在这个场合,Mock对象在测试中有更广泛的应用。几乎完善的单元测试,离不开Mock对象。

 

于是,我尝试把我的数据访问代码做成接口(Interface),比如说 IProductDAL 接口。当真实应用程序运行时,我会使用"真实"的接口实现(比如说,ProductDALImpl),去访问实际的数据库;如果是单元测试运行时,我会另外做一个"虚假"的实现(比如说,ProductDALMockImpl),实现接口的代码并不真正访问数据库,而只是返回我要的数据。这样一来就可以对使用IProductDAL接口的代码(比如说是ProductBusiness代码)进行单元测试了。当然,ProductBusiness中应该应用了"针对接口编程,而不是具体实现"的面向对象设计原则。在ProductBusiness也作了一些处理,方便我们对其进行单元测试。为ProductBusiness增加一个IProductDAL参数的构造函数 internal ProductBusiness(IProductDAL productDAL)和一个internal SetProductDAL(IProductDAL productDAL)。这样就可以在ProductBusiness构造时和构造后指定 IProductDAL 的实现。如下所示:

 

public ProductBusiness
{
    private IProductDAL _productDAL;

 

    public ProductBusiness()
    {
         _productDAL = new ProductDALImpl();
         //...
    }

 

    internal ProductBusiness(IProductDAL productDAL)
    {
         //...
         _productDAL = productDAL;
    }

 

    internal SetProductDAL(IProductDAL productDAL)
    {
        _productDAL = productDAL;
    }
}

 

而且在ProductBusiness代码所在的项目中的AssemblyInfo.cs文件中添加[assembly:InternalsVisibleTo("UnitTestDllName")]
这样在ProductBusiness类中为单元测试而新添加的两个internal方法不会对它的外部项目的客户(使用这个类的代码)产生影响(internal的方法对外部项目不可见),但在单元测试的项目中却又可以使用这两个internal方法。用这两个方法,方便地把我的Mock对象,IProductDAL的替身换上。用internal方法的一个缺点是,如果ProductBusiness中用了很多这样的接口,就得添加很这样的internal方法。

 

除了添加这些internal方法的方案,还可以使用 IoC Container 框架实现。ProductBusiness类的默认构造函数中 _productDAL = new ProductDALImpl();的 new ProductDALImpl() 改用IoC Container来生成IProductDAL的实例。这样就可以通过配置来控制生成的实例。在应用程序运行时就配置成"真实"的实现,而单元测试时配置成一个"虚假"的Mock实现。这样就不用在ProductBusiness中为代码的可测试性而添加internal方法了。

 

但是要写很多的Mock对象,做很多的替代也真是件麻烦事。于是又从网友那里得知可以使用Mock对象框架。于是我google了一下,找到了几个Mock框架,挑了一两个玩了一下,感觉的确很强大。虽然网上有许多免费的Mock框架,但我觉得做得最好的,还是收费的TypeMock,它给我的振憾最大,印象最深。Mock框架的使用大家可以google一下相关资料。

 

上面讲的Mock方法,不仅在和数据访问代码相关单元测试中使用,它的应用要广泛的多。

 

有时发现把每个数据访问都做成接口也是件麻烦事。虽然这样做可能有些别的好处,但我却一直看着它的坏处。所以总觉得这样的解决方案不够舒服。而且也不能对数据访问代码本身进行测试。

 

对数据访问代码进行测试的一个可行的办法是每次运行一个单元测试方法前将数据库重置,把所有表Drop掉,再重新创建。再插入一些测试数据,随后完成一个测试;再数据库重置,再运行下一个测试;...... 这样就可以对数据库访问代码也可以测试了。这个方法看起来很不错。问题是如果单元测试有成千上万个时,每次都把数据库这样折腾一下,性能上很不好,感觉上就不是很舒服了。如果每次测试时又只删除跟当前测试相关的特定表又谦麻烦。如果能把这个方法的好处保留,又能在性能上有革命性的提高,那应该很不错。

 

又过了一段时间,我对ORM框架兴趣大为增加。Microsoft Entity Framework(简称EF),NHibernate(简称NH)都好好地玩了一把。得到的感受是,我再也不会原始地用Connection和Command对象来进行数据库访问了。不仅老土,还会有维护问题,开发效率也不高。更棒的是用大部分ORM框架写的代码还可以与具体数据库无关,这一点EF和NH都能做到。

 

于是又回到单元测试这个问题上,现在使用EF和NH写代码,这些代码和具体数据库是无关的,切换一个数据库的工作量几乎是零。也就是说,单元测试时的数据库可以和程序真正运行时的数据库可以是不同的。

 

听说Java中有一个很有名的内存数据库HSQL, 进这个数据库访问时没有磁盘I/O,一切都在内存中进行,访问的速度非常快。.Net中有没有这样的数据库呢?google了一下,发现原来SQLite就有这样的功能且支持的SQL和标准的SQL基本兼容而且NH对其也有官方支持。只需在对SQLite的连接字符串中把数据库文件指定为:memory:就可以了。真的为之一振!但玩着玩着问题就出现了,只要Connection一关闭这个内存数据库被清除了,重新连接时又初始化了一个。而一个测试所包括的代码中往往包含了许多Connection的打开和关闭。所以用SQLite的Memory模式还是不行。有些内存实时数据库的性能可达纳秒级别,可惜他们大部分均不采用标准SQL的访问方式,而且.Net访问它们有时也不一定很方便。

 

现在的问题主题数据库的速度上面。想起以前我机器慢,为了让VC++能编译得快点,我想过许多办法。包括把VC++的工程中的文件全部搬到内存盘中,来提高C++的编译速度。内存盘,对了就是它!把数据放在内存盘中,这样速度上肯定会有很大的提高。当测试量很大时,它可以减少上万,上10万的碰盘I/O,太棒了!

 

但总不能把一个Oracle数据库放入一个RAM Disk中吧?!所以还得选一个合适的数据库。EF和NH都能做到数据库无关,所以选一个合适的作为开发和测试的数据库,将来再迁移到Oracle,DB2等大型数据库无非就是改个配置。这样的数据库应该支持主流ORM框架,性能高,最好还支持嵌入式数据库。如果采用团队共享服务器式的数据库模式,当团队中的不同成员运行单元测试时,会有数据冲突。嵌入式的数据库不仅可以在团队成员的机器上免安装运行,还能把各自运行的数据隔离在自己的机器上。前面的SQLite已经很程度上满足要求了。不采用它的Memory模式,但把数据库文件放在RAM Disk中。可我还想找个更好的。

 

经过一阵google后,我发现Firebird数据库非常适合。Firebird是一个开源数据库,源自商业的InterBase。以前用Delphi,C++ Builder时常常会和InterBase碰上。因为他们同是Borland的产品。Firebird基本上兼容了InterBase,可它免费还开源。Firebird对标准SQL支持很好,性能很高,是一个完善健全的数据库。官方提供的.Net Provider还支持EF,NH对Firebird也有官方支持。而且Firebird不仅支持服务器模式,还支持嵌入式模式。嵌入模式同样高性能,支持高并发。以数据访问代码来说,服务器模式和嵌入模式的区别仅在于连接字符串上,其它访问接口完全一致。Firebird的数据库文件可以是一个单一的*.fdb文件,非常便于放入RAM Disk中。真是太好啦!

 

最后,我确定了一种对数据库访问代码进行测试的个人模式。使用NH,EF访问数据库,使用Firebird Embedded作为单元测试用数据库。当单元测试时有数据库性能问题时,就把数据库文件放入RAM Disk,减少磁盘I/O,加快运行。如果团队成员每次只运行很少的几个单元测试,也就没必要做个RAM Disk。每个单元测试开始时都用NH中的SchemaExport自动生成数据库(EF的最新版本中也将提供类似功能)或可以运行一段Drop表和Create表的SQL脚本。我更喜欢SchemaExport,因为那连SQL脚本也不用人工创建和维护,一切都是自动的。可以把这个进行数据库表定义的准备活动在单元测试的基类中进行,让它在每个测试开始前都进行这个数据库表定义准备,而且这样的代码只需写一个地方。几乎所有这单元测试框架都提供了在每个测试前后运行某个特定代码的功能,具体可以看使用的单元测试框架的文档。在数库表定义完成后,在每个测试的内部作再在这个"干清"的数据库上做测试数据的准备,因为每个测试所需的数据往往是不同的。测试数据的准备可以用运行一段包含Insert的SQL的方法,但我更喜欢用ORM的数据插入方法,这样单元测试的维护性也更好,开发效率更高,运行效率也很好!

原创粉丝点击