【问题标题】:Unit testing with Mocks when SUT is leveraging Task Parallel LibaraySUIT 利用任务并行库时使用 Mocks 进行单元测试
【发布时间】:2010-04-29 10:53:15
【问题描述】:

我正在尝试对被测系统 (SUT) 对依赖项调用方法进行单元测试/验证。

  • 依赖是 IFoo。
  • 依赖类是 IBar。
  • IBar 被实现为 Bar。
  • 当在 Bar 实例上调用 Start() 时,Bar 将在新的 (System.Threading.Tasks.)Task 中调用 IFoo 上的 Start()。

单元测试(起订量):

    [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 作为 Bar 的实现:

    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)*

【问题讨论】:

  • 有什么特别的理由不自己编写IFoo 的简单模拟实现并改用它吗?

标签: c# .net unit-testing .net-4.0 moq


【解决方案1】:

@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 也可以让您运行确定性的“单元测试”。

【讨论】:

  • 我同意您关于我的解决方案的初始观点。我想我会选择使用事件而不是调度程序,但无论哪种方式,都比我的答案更好。
  • 不幸的是,这不再有效,因为 System.Threading.Tasks.TaskScheduler 上的所有方法都已密封。
  • @Richard:注意注入的类型是 TestScheduler 实现的接口 IScheduler。我不是要覆盖 TaskSheduler 上的方法,而是利用 TaskScheduler 和 TestScheduler 都实现 IScheduler 接口并且可以互换的事实。
  • 啊抱歉@Richard,我看到我们在谈论两种不同的类型,名称相同。我说的是 System.Reactive.Concurrency.IScheduler 和 System.Reactive.Concurrency.Scheduler.TaskPool,它公开了 System.Reactive.Concurrency.TaskPoolScheduler 类型。所以这一切都在 Reactive Extension 框架中。太好了,看看吧。
【解决方案2】:

您的测试使用了太多的实现细节,IEnumerable&lt;IFoo&gt; 类型。每当我必须开始使用 IEnumerable 进行测试时,总会产生一些摩擦。

【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2019-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多