繁体   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