簡體   English   中英

當SUT利用Task Parallel Libaray時,使用Mocks進行單元測試

[英]Unit testing with Mocks when SUT is leveraging Task Parallel Libaray

我正在嘗試通過被測系統(SUT)對依賴性調用方法進行單元測試/驗證。

  • 依賴性是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();
    }

IBar作為酒吧的實施:

    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例外:

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

@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也允許您運行確定性的“單元測試”。

您的測試使用了太多的實現細節, IEnumerable<IFoo>類型。 每當我必須開始使用IEnumerable進行測試時,它總會產生一些摩擦。

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();
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM