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

发布于 2024-08-30 13:25:17 字数 1744 浏览 2 评论 0原文

我正在尝试进行单元测试/验证被测系统 (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)*

I am trying to unit test / verify that a method is being called on a dependency, by the system under test (SUT).

  • The depenedency is IFoo.
  • The dependent class is IBar.
  • IBar is implemented as Bar.
  • Bar will call Start() on IFoo in a new (System.Threading.Tasks.)Task, when Start() is called on Bar instance.

Unit Test (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:

    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.MockVerificationException : The following setups were not matched:
IFoo foo => foo.Start() (StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() in
FooBarTests.cs: line 19)*

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

可是我不能没有你 2024-09-06 13:25:17

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

@dpurrington & @StevenH: If we start putting this kind of stuff in our code

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.

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.

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.

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. 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.

就像说晚安 2024-09-06 13:25:17

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

Your tests uses too much implementation detail, IEnumerable<IFoo> types. Whenever I have to start testing with IEnumerable it always creates some friction.

ペ泪落弦音 2024-09-06 13:25:17

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

Thread.Sleep() is definitely a bad idea. 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). 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. 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.

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

In this example, FooCountdown will wait to be signaled 3 times if there are 3 foo objects in Foos.

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().

[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();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文