求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
并行运行单元测试的启示
 
作者:捷道 , 发布于2012-4-11
 

我们希望采用并行的方式在本地运行单元测试,从而减少测试时间,提高开发人员的工作效率。我们使用了线程池来提供多线程的并行任务。通过配置启动多个线程,并以程序集为单位,启动TestRunner:

var executorWrapper=newExcetorWrapper(assemblyName,null,false);

var testRunner=newTestRunner(executorWrapper,newRunnerLoggerWrapper());

testRunner.RunAssembly();

其中的RunnerLoggerWrapper是一个自定义的类,实现了Xunit的IRunnerLogger。XUnit的使用并非本文描述的内容,在此略过。

因为是以程序集为单位,所以我们在启动多线程之前,会事先将需要运行的程序集放到一个队列中,然后在启动多线程之后,执行出队列操作。多线程的运行代码如下所示:

privatestaticManualResetEvent[] resetEvents;

privatestaticQueue<String>assemblyQue;

privatestaticreadonlyObject LockAssembly2Queue=newObject();

publicvoidRun()

{

for(var index=0; index<numThreads; index++)

{

resetEvents[index]=newManualResetEvent(false);

ThreadPool.QueueUserWorkItem(DoWork, index);

}

WaitForAllManualEvent();

}

privatevoidWaitForAllManualEvent()

{

if(Thread.CurrentThread.ApartmentState=ApartmentState.STA)

{

foreach(var manualResetEventinresetEvents)

{

    WaitHandle.WaitAny(newWaitHandle[]{manualResetEvent});

}

}

else

{

WaitHandle.WaitAll(resetEvents);

}

}

privatestaticvoidDoWork(Object index)

{

Thread.CurrentThread.ApartmentState=ApartmentState.STA;

while(true)

{

stringcurrentAssemblyName=null;

lock(LockAssembly2Queue)

{

    if(assemblyQue.Count!=0)

    {

        currentAssemblyName=assemblyQue.Dequeue();

    }

    else

    {

        resetEvents[(int)index].Set();

        Console.WriteLine("Exited current thread:{0}", Thread.CurrentThread.Name);

        break;

        }

}

if(currentAssemblyName!=null)

{

    newTestRunnerWrapperWithAssembly(currentAssemblyName).Runner();

}

}

}

由于要测试的程序集比较多,采用这种并行方式可以极大地提高运行效率。由于单元测试彼此是独立的,在并行运行时,互相没有干扰。这是我们实现判断的结果。一切看起来很美好,但在真正运行时,却出现了大量的死锁。异常信息为:

Transaction (Process ID) was deadlocked on resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

在我们的单元测试中,大多数测试需要访问的资源都是在内存中进行,但有一部分单元测试必须与数据库通信,对数据表进行读写。除了极少数特殊的测试用例外,对数据表的操作都放在事务中进行,并在执行完毕后,通过回滚事务,避免对真实数据的提交,保证单元测试不会影响数据库。

注:单元测试应该访问数据库吗?这其实还有待确认。在《修改代码的艺术》一书中,Feathers这样写道:

单元测试运行得快。运行得不快的不是单元测试。

有些测试容易跟单元测试混淆起来。譬如下面这些测试就不是单元测试:

(1)跟数据库有交互;

(2)进行了网络间通信;

(3)调用了文件系统;

(4)需要你对环境作特定的准备(如编辑配置文件)才能运行的。

以上可以看到Feathers的态度是单元测试不应与外部资源进行交互。显然,如果出现了这些交互,就应该采用Mock的方式来模拟对外部资源的访问。然而,某些实现功能却是与外部资源息息相关,又或者我们测试的目的本身就是验证对外部资源的访问是否正确。从测试的范围来看,它们仍然算是单元测试,但因其特殊性,而应该将这些测试放到系统测试的范畴。在持续集成中,我们常常用金字塔来表示单元测试、系统测试和集成测试的数量。如下图所示:

单元测试的数量最多,如果还需要访问外部资源,就会严重影响运行单元测试的速度。关于单元测试、Mock等内容,我希望在以后的文章里详细论述。

在我们的项目中,是通过注入Fixture的形式生成测试数据。例如,我们可能希望注入Client、Associate等对象,从而完成对某些行为的测试。例如:

[Fixture(typeof(client_hastings))]
public Client client;

[Fixture(typeof(Samuel))]

public Associate Samuel;

通过Fixture准备数据时,如果采用了持久化方式,则意味着需要对数据表进行操作。如上代码就可能操作多张表,例如对Client表和Associate表进行写操作。由于单元测试采用并行方式进行。假设存在两个单元测试均需要对Client和Associate注入Fixture,生成测试数据;并且不幸的是,这两个测试用例准备数据的顺序刚好相反,即A测试用例的顺序为Client->Associate,B测试用例的顺序为Associate->Client,就可能发生死锁。

为什么?让我们分析数据库发生死锁的情况。它必然是多个进程(或线程)对两个或两个以上的资源形成了交叉访问。例如进程A在占有了资源1的同时,还需要访问资源2;与此同时,进程B在占有了资源2的同时,需要访问资源1。由于资源1已经被进程A占用,无法释放,进程B就会等待;而进程A希望访问的资源2又被等待中的进程B持有;二者互不相让,最终产生死锁。这正是并行运行单元测试导致死锁的根本原因。我们可以运行SQL Server Profiler来监视数据库的执行。注意,倘若需要跟踪死锁的情况,需要在Trace Properties中勾选“Deadlock Graph”和“Lock: Deallock”选项,如下图所示:

创建Trace后,利用并行方式运行单元测试,可能得到这样的Deadlock graph:

图中,椭圆代表进程(线程),矩形代表资源。左边的椭圆打了一把叉,说明是竞争失败的进程(线程)。从椭圆出发,箭头所指的资源,代表进程请求的资源;而发出箭头的资源,则代表箭头指向的进程持有该资源。可以发现,两个进程与两个资源之间的箭头,事实上形成了一个封闭的环。这正是死锁的典型表现。

当我们将单元测试的Fixture注入顺序保持一致时,这样的死锁就能够避免了。这是一种限制,它很难被编写单元测试的开发人员所接受,即使勉强接受,仍然很容易疏漏。因此,我们的结论仍然是“不到迫不得已,单元测试不要访问外部资源”。或者说,我们可以将访问外部资源的单元测试,看成是特殊的单元测试,如果确实需要并行运行测试,以提高测试效率,可以通过引入多个Agent,以物理方式隔离资源,避免出现资源的争用导致死锁。

那么,这是否意味着我们的产品代码不够严谨,没有充分考虑并发的情况呢?不完全对。因为这里产生死锁的时机发生在准备测试数据的阶段,实际操作时,一般不会出现这种频繁对多张表进行操作的情况。然而,即使几率很低,始终存在死锁的隐患,这就为我们的开发敲响了警钟。因此在开发过程中,有必要通过对业务的分析,制订一些指导原则,通过规范写数据表操作的顺序,避免出现死锁。这是这次并行运行单元测试给我们带来的启示。


相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...