2010-04-29 45 views
9

我想单元测试/验证一个方法正在被测系统(SUT)上的依赖关系上调用。当SUT利用任务并行Libaray进行单元测试Mock

  • 依赖性是IFoo。
  • 依赖类是IBar。
  • IBar以Bar的形式实现。
  • 当在Bar实例上调用Start()时,Bar会在新的(System.Threading.Tasks。)任务中调用IFoo的Start()。

单元测试(MOQ):

[Test] 
    public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
    { 
     //ARRANGE 

     //Create a foo, and setup expectation 
     var mockFoo0 = new Mock<IFoo>(); 
     mockFoo0.Setup(foo => foo.Start()); 

     var mockFoo1 = new Mock<IFoo>(); 
     mockFoo1.Setup(foo => foo.Start()); 


     //Add mockobjects to a collection 
     var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

     IBar sutBar = new Bar(foos); 

     //ACT 
     sutBar.Start(); //Should call mockFoo.Start() 

     //ASSERT 
     mockFoo0.VerifyAll(); 
     mockFoo1.VerifyAll(); 
    } 

伊巴尔的实施,酒吧:

class Bar : IBar 
    { 
     private IEnumerable<IFoo> Foos { get; set; } 

     public Bar(IEnumerable<IFoo> foos) 
     { 
      Foos = foos; 
     } 

     public void Start() 
     { 
      foreach(var foo in Foos) 
      { 
       Task.Factory.StartNew(
        () => 
         { 
          foo.Start(); 
         }); 
      } 
     } 
    } 

起订量异常:

*Moq.MockVerificationException : The following setups were not matched: 
IFoo foo => foo.Start() (StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() in 
FooBarTests.cs: line 19)* 
+2

是否有什么特别的原因不要自己编写一个简单的'IFoo'模拟实现并使用它呢? – 2010-04-29 11:46:21

回答

7

@dpurrington & @StevenH:如​​果我们开始把这种东西在我们的代码

sut.Start(); 
Thread.Sleep(TimeSpan.FromSeconds(1)); 

,我们有成千上万的“单位”测试,那么我们的测试开始运行到分钟而不是秒。如果你有例如1000个单元测试,如果有人走了,用Thread.Sleep散布测试代码库,很难让你的测试在5秒内运行。

我建议这是不好的做法,除非我们明确地进行集成测试。

我的建议是使用System.CoreEx.dll中的System.Concurrency.IScheduler接口并注入TaskPoolScheduler实现。

这是我应如何实现

using System.Collections.Generic; 
using System.Concurrency; 
using Moq; 
using NUnit.Framework; 

namespace StackOverflowScratchPad 
{ 
    public interface IBar 
    { 
     void Start(IEnumerable<IFoo> foos); 
    } 

    public interface IFoo 
    { 
     void Start(); 
    } 

    public class Bar : IBar 
    { 
     private readonly IScheduler _scheduler; 

     public Bar(IScheduler scheduler) 
     { 
      _scheduler = scheduler; 
     } 

     public void Start(IEnumerable<IFoo> foos) 
     { 
      foreach (var foo in foos) 
      { 
       var foo1 = foo; //Save to local copy, as to not access modified closure. 
       _scheduler.Schedule(foo1.Start); 
      } 
     } 
    } 

    [TestFixture] 
    public class MyTestClass 
    { 
     [Test] 
     public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
     { 
      //ARRANGE 
      TestScheduler scheduler = new TestScheduler(); 
      IBar sutBar = new Bar(scheduler); 

      //Create a foo, and setup expectation 
      var mockFoo0 = new Mock<IFoo>(); 
      mockFoo0.Setup(foo => foo.Start()); 

      var mockFoo1 = new Mock<IFoo>(); 
      mockFoo1.Setup(foo => foo.Start()); 

      //Add mockobjects to a collection 
      var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

      //ACT 
      sutBar.Start(foos); //Should call mockFoo.Start() 
      scheduler.Run(); 

      //ASSERT 
      mockFoo0.VerifyAll(); 
      mockFoo1.VerifyAll(); 
     } 
    } 
} 

现在,这允许测试全速没有任何的Thread.Sleep运行建议。

请注意,合同已被修改为接受Bar构造函数中的IScheduler(用于依赖注入),并且IEnumerable现在传递给IBar.Start方法。我希望这是有道理的,为什么我做了这些改变。

测试速度是这样做的第一个也是最明显的好处。这样做的第二个可能更重要的好处是,当您向代码中引入更复杂的并发时,这使得测试非常困难。即使面对更复杂的并发,IScheduler接口和TestScheduler也允许您运行确定性的“单元测试”。

+0

我同意你关于我的解决方案的初始观点。我想我会选择使用事件而不是调度器,但无论哪种方式,都比我的回答好。 – dpurrington 2011-09-30 13:13:21

+0

不幸的是,这不再起作用,因为[System.Threading.Tasks.TaskScheduler](http://msdn.microsoft.com/zh-cn/library/system.threading.tasks.taskscheduler.aspx)上的所有方法都是密封的。 – 2011-12-05 14:56:04

+2

@Richard:注意被注入的类型是TestScheduler实现的接口IScheduler。我不想重写TaskSheduler上的方法,我利用了TaskScheduler和TestScheduler都实现了IScheduler接口的事实,因此它们是可以互换的。 – 2011-12-05 17:48:26

0

你的测试使用太多的实现细节, IEnumerable<IFoo>类型。每当我必须开始使用IEnumerable进行测试时,它总会产生一些摩擦。

0

Thread.Sleep()绝对是一个坏主意。我已经阅读过几次,“真正的应用程序不会睡觉”。尽你所能,但我同意这种说法。特别是在单元测试中。如果你的测试代码产生错误的失败,你的测试很脆弱。

我最近写了一些测试,正确地等待并行任务完成执行,我想我会分享我的解决方案。我意识到这是一个旧帖子,但我认为它会为寻求解决方案的人们提供价值。

我的实现涉及修改被测试的类和被测试的方法。

class Bar : IBar 
{ 
    private IEnumerable<IFoo> Foos { get; set; } 
    internal CountdownEvent FooCountdown; 

    public Bar(IEnumerable<IFoo> foos) 
    { 
     Foos = foos; 
    } 

    public void Start() 
    { 
     FooCountdown = new CountdownEvent(foo.Count); 

     foreach(var foo in Foos) 
     { 
      Task.Factory.StartNew(() => 
      { 
       foo.Start(); 

       // once a worker method completes, we signal the countdown 
       FooCountdown.Signal(); 
      }); 
     } 
    } 
} 

CountdownEvent对象是很方便的,当你有执行多个并行任务,你需要等待完成(比如,当我们迫不及待地试图在单元测试中断言)。构造函数在发出等待处理完成代码的信号之前,初始化它应该发送的次数。

内部访问修饰符用于CountdownEvent的原因是因为我通常在单元测试需要访问它们时将属性和方法设置为内部。然后,我在测试的Properties\AssemblyInfo.cs文件中的程序集中添加一个新的程序集属性,以便将内部结构暴露给测试项目。

[assembly: InternalsVisibleTo("FooNamespace.UnitTests")] 

在这个例子中,如果Foos中有3个foo对象,FooCountdown将等待3次发送信号。

现在这就是您如何等待FooCountdown完成信号处理,以便您可以继续生活并在Thread.Sleep()上停止浪费CPU周期。

[Test] 
public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
{ 
    //ARRANGE 

    var mockFoo0 = new Mock<IFoo>(); 
    mockFoo0.Setup(foo => foo.Start()); 

    var mockFoo1 = new Mock<IFoo>(); 
    mockFoo1.Setup(foo => foo.Start()); 


    //Add mockobjects to a collection 
    var foos = new List<IFoo> { mockFoo0.Object, mockFoo1.Object }; 

    IBar sutBar = new Bar(foos); 

    //ACT 
    sutBar.Start(); //Should call mockFoo.Start() 
    sutBar.FooCountdown.Wait(); // this blocks until all parallel tasks in sutBar complete 

    //ASSERT 
    mockFoo0.VerifyAll(); 
    mockFoo1.VerifyAll(); 
}