2010-04-29 59 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(); 
}