当SUT利用Task Parallel Libaray时,使用Mocks进行单元测试

hol*_*see 9 .net c# unit-testing moq .net-4.0

我正在尝试通过被测系统(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();
    }
Run Code Online (Sandbox Code Playgroud)

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();
                        });
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

Moq例外:

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

Lee*_*ell 7

@dpurrington和@StevenH:如​​果我们开始在我们的代码中加入这种东西

sut.Start();
Thread.Sleep(TimeSpan.FromSeconds(1)); 
Run Code Online (Sandbox Code Playgroud)

我们有成千上万的"单位"测试然后我们的测试开始运行到分钟而不是几秒钟.如果您有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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,这允许测试在没有任何Thread.Sleep的情况下全速运行.

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

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

  • @Richard:请注意,注入的类型是TestScheduler实现的接口IScheduler.我不是试图覆盖TaskSheduler上的方法,我正在利用TaskScheduler和TestScheduler实现IScheduler接口的事实,因此可以互换. (2认同)