简体   繁体   English

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

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

I am trying to unit test / verify that a method is being called on a dependency, by the system under test (SUT). 我正在尝试通过被测系统(SUT)对依赖性调用方法进行单元测试/验证。

  • The depenedency is IFoo. 依赖性是IFoo。
  • The dependent class is IBar. 依赖类是IBar。
  • IBar is implemented as Bar. IBar实现为Bar。
  • Bar will call Start() on IFoo in a new (System.Threading.Tasks.)Task, when Start() is called on Bar instance. 当在Bar实例上调用Start()时,Bar将在新的(System.Threading.Tasks。)任务中调用IFoo上的Start()。

Unit Test (Moq): 单元测试(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();
    }

Implementation of IBar as Bar: 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 Exception: Moq例外:

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

@dpurrington & @StevenH: If we start putting this kind of stuff in our code @dpurrington和@StevenH:如​​果我们开始在我们的代码中加入这种东西

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

and we have thousands of "unit" tests then our tests start running into the minutes instead of seconds. 我们有成千上万的“单位”测试然后我们的测试开始运行到分钟而不是几秒钟。 If you had for example 1000 unit tests, it will be hard to have your tests to run in under 5 seconds if someone has gone and littered the test code base with Thread.Sleep. 如果您有1000个单元测试,如果有人已经离开并使用Thread.Sleep乱丢测试代码库,那么很难让测试在5秒内运行。

I suggest that this is bad practice, unless we are explicitly doing Integration testing. 我建议这是不好的做法,除非我们明确地进行集成测试。

My suggestion would be to use the System.Concurrency.IScheduler interface from System.CoreEx.dll and inject the TaskPoolScheduler implementation. 我的建议是使用System.CoreEx.dll的System.Concurrency.IScheduler接口并注入TaskPoolScheduler实现。

This is my suggestion for how this should be implemented 这是我对如何实施这一建议的建议

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

This now allows the test to run at full speed without any Thread.Sleep. 现在,这允许测试在没有任何Thread.Sleep的情况下全速运行。

Note that the contracts have been modified to accept an IScheduler in the Bar constructor (for Dependency Injection) and the IEnumerable is now passed to the IBar.Start method. 请注意,合同已被修改为接受Bar构造函数中的IScheduler(用于依赖注入),IEnumerable现在传递给IBar.Start方法。 I hope this makes sense why I made these changes. 我希望这是有道理的,为什么我做了这些改变。

Speed of testing is the first and most obvious benefit of doing this. 测试速度是这样做的第一个也是最明显的好处。 The second and possibly more important benefit of doing this is when you introduce more complex concurrency to your code, which makes testing notoriously difficult. 这样做的第二个也可能是更重要的好处是当你为代码引入更复杂的并发时,这使得测试变得非常困难。 The IScheduler interface and the TestScheduler can allow you to run deterministic "unit tests" even in the face of more complex concurrency. 即使面对更复杂的并发性,IScheduler接口和TestScheduler也允许您运行确定性的“单元测试”。

Your tests uses too much implementation detail, IEnumerable<IFoo> types. 您的测试使用了太多的实现细节, IEnumerable<IFoo>类型。 Whenever I have to start testing with IEnumerable it always creates some friction. 每当我必须开始使用IEnumerable进行测试时,它总会产生一些摩擦。

Thread.Sleep() is definitely a bad idea. Thread.Sleep()绝对是个坏主意。 I've read on SO several times over that "Real apps don't sleep". 我已经多次读过“真正的应用程序不会睡觉”。 Take that as you will but I agree with that statement. 按照你的意愿行事,但我同意这一说法。 Especially during unit tests. 特别是在单元测试中。 If your test code creates false failures, your tests are brittle. 如果您的测试代码产生错误的失败,那么您的测试就会很脆弱。

I wrote some tests recently that properly wait for parallel tasks to finish executing and I thought I would share my solution. 我最近写了一些测试,等待并行任务完成执行,我想我会分享我的解决方案。 I realize this is an old post but I thought it would provide value for those looking for a solution. 我意识到这是一个老帖子,但我认为它会为那些寻找解决方案的人提供价值。

My implementation involves modifying the class under test and the method under test. 我的实现涉及修改测试中的类和测试中的方法。

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 objects are handy when you have multiple parallel tasks executing and you need to wait for completion (like when we wait to attempt an assert in unit tests). 当您执行多个并行任务并且需要等待完成时(例如,当我们等待在单元测试中尝试断言时),CountdownEvent对象很方便。 The constructor initializes with the number of times it should be signaled before it signals waiting code that processing is complete. 构造函数在信号等待处理完成的代码之前用应该发信号的次数进行初始化。

The reason the internal access modifier is used for the CountdownEvent is because I usually set properties and methods to internal when unit tests need access to them. 内部访问修饰符用于CountdownEvent的原因是因为我通常在单元测试需要访问它们时将属性和方法设置为内部。 I then add a new assembly attribute in the assembly under test's Properties\\AssemblyInfo.cs file so the internals are exposed to a test project. 然后,我在测试的Properties\\AssemblyInfo.cs文件中的程序集中添加一个新的程序集属性,以便内部公开给测试项目。

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

In this example, FooCountdown will wait to be signaled 3 times if there are 3 foo objects in Foos. 在这个例子中,如果Foos中有3个foo对象,FooCountdown将等待3次发出信号。

Now this is how you wait for FooCountdown to signal processing completion so you can move on with your life and quit wasting cpu cycles on Thread.Sleep(). 现在,这就是你等待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