并行运行单元测试的启示

发表于:2015-03-27来源:uml.org.cn作者:捷道点击数: 标签:单元测试
我们希望采用并行的方式在本地运行单元测试,从而减少测试时间,提高开发人员的工作效率。我们使用了线程池来提供多线程的并行任务。通过配置启动多个线程,并以程序集为单位

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

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

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

  testRunner.RunAssembly();

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

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

  privatestaticManualResetEvent[] resetEvents;

  privatestaticQueueassemblyQue;

  privatestaticreadonlyObject LockAssembly2Queue=newObject();

  publicvoidRun()

  {

  for(var index=0; 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等内容,我希望在以后的文章里详细论述。

原文转自:http://www.uml.org.cn/Test/201204112.asp