代码之家  ›  专栏  ›  技术社区  ›  holsee

当SUT利用任务并行库时使用mock进行单元测试

  •  9
  • holsee  · 技术社区  · 15 年前

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

    • 依赖性是IFoo。
    • 依赖类是IBar。

    单元测试(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作为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)*
    
    3 回复  |  直到 15 年前
        1
  •  7
  •   casperOne    12 年前

    @德普林顿@史蒂文:如果我们开始在代码中加入这种东西

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

    我们有数千个“单元”测试,然后我们的测试开始运行到几分钟而不是几秒钟。例如,如果您有1000个单元测试,那么如果有人在测试代码库中添加了Thread.Sleep,那么您的测试很难在5秒内运行。

    我认为这是一种糟糕的做法,除非我们明确地进行集成测试。

    这是我对如何实施的建议

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

    注意,契约已经被修改为接受Bar构造函数中的isScheduler(用于依赖注入),IEnumerable现在被传递给IBar.Start方法。我希望这能解释为什么我做了这些改变。

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

        2
  •  0
  •   Gutzofter    15 年前

    你的测试使用了太多的实现细节, IEnumerable<IFoo>

        3
  •  0
  •   Jeff LaFay    13 年前

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

    internal access修饰符用于CountdownEvent的原因是,当单元测试需要访问属性和方法时,我通常将它们设置为internal。然后,我在被测试的程序集中添加一个新的程序集属性 Properties\AssemblyInfo.cs

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

    在本例中,如果Foos中有3个foo对象,footcountdown将等待3次信号通知。

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