Moq is slow to verify dependency after a large number calls

♀尐吖头ヾ 提交于 2020-07-22 21:31:30

问题


I've hit a snag when using Moq to simulate an dependency which is called a large number of times. When I call Verify, Moq takes a long time (several minutes) to respond, and sometimes crashes with a NullReferenceException (I guess this is understandable, given the amount of data that Moq would have to accumulate to do the Verify from a "cold start").

So my question is, is there another strategy that I can use to do this using Moq, or should I revert to a hand-crafted stub for this rather unusual case. Specifically, is there a way to tell Moq up front that I'm only interested in verifying specific filters on the parameters, and to ignore all other values?

Neither of the approaches below is satisfactory.

Given CUT and Dep:

public interface ISomeInterface
{
    void SomeMethod(int someValue);
}

public class ClassUnderTest
{
    private readonly ISomeInterface _dep;
    public ClassUnderTest(ISomeInterface dep)
    {
        _dep = dep;
    }

    public void DoWork()
    {
        for (var i = 0; i < 1000000; i++) // Large number of calls to dep
        {
            _dep.SomeMethod(i);
        }
    }
}

Moq Strategy 1 - Verify

        var mockSF = new Mock<ISomeInterface>();
        var cut = new ClassUnderTest(mockSF.Object);
        cut.DoWork();
        mockSF.Verify(mockInt => mockInt.SomeMethod(It.Is<int>(i => i == 12345)),
                      Times.Once());
        mockSF.Verify(mockInt => mockInt.SomeMethod(It.Is<int>(i => i == -1)),
                      Times.Never());

Moq Strategy 2 - Callback

        var mockSF = new Mock<ISomeInterface>();
        var cut = new ClassUnderTest(mockSF.Object);
        bool isGoodValueAlreadyUsed = false;
        mockSF.Setup(mockInt => mockInt.SomeMethod(It.Is<int>(i => i == 12345)))
              .Callback(() =>
            {
                if (isGoodValueAlreadyUsed)
                {
                    throw new InvalidOperationException();
                }
                isGoodValueAlreadyUsed = true;
            });
        mockSF.Setup(mockInt => mockInt.SomeMethod(It.Is<int>(i => i == -1)))
              .Callback(() =>
                { throw new InvalidOperationException(); });

        cut.DoWork();
        Assert.IsTrue(isGoodValueAlreadyUsed);

回答1:


Usually when such a limitation is reached, I would reconsider my design (no offense, I see your rep). Looks like the method under test does too much work, which is violation of the single responsibility principle. It first generates a large list of items, and then verifies a worker is called for each one of them, while also verifying that the sequence contains the right elements.

I'd split the functionality into a sequence generator, and verify that the sequence has the right elements, and another method which acts on the sequence, and verify that it executes the worker for each element:

namespace StackOverflowExample.Moq
{
    public interface ISequenceGenerator
    {
        IEnumerable<int> GetSequence();
    }

    public class SequenceGenrator : ISequenceGenerator
    {
        public IEnumerable<int> GetSequence()
        {
            var list = new List<int>();
            for (var i = 0; i < 1000000; i++) // Large number of calls to dep
            {
                list.Add(i);
            }
            return list;
        }
    }

    public interface ISomeInterface
    {
        void SomeMethod(int someValue);
    }

    public class ClassUnderTest
    {
        private readonly ISequenceGenerator _generator;
        private readonly ISomeInterface _dep;

        public ClassUnderTest(ISomeInterface dep, ISequenceGenerator generator)
        {
            _dep = dep;
            _generator = generator;
        }

        public void DoWork()
        {
            foreach (var i in _generator.GetSequence())
            {
                _dep.SomeMethod(i);
            }
        }
    }

    [TestFixture]
    public class LargeSequence
    {
        [Test]
        public void SequenceGenerator_should_()
        {
            //arrange
            var generator = new SequenceGenrator();

            //act
            var list = generator.GetSequence();

            //assert
            list.Should().Not.Contain(-1);
            Executing.This(() => list.Single(i => i == 12345)).Should().NotThrow();
            //any other assertions
        }

        [Test]
        public void DoWork_should_perform_action_on_each_element_from_generator()
        {
            //arrange
            var items = new List<int> {1, 2, 3}; //can use autofixture to generate random lists
            var generator = Mock.Of<ISequenceGenerator>(g => g.GetSequence() == items);
            var mockSF = new Mock<ISomeInterface>();

            var classUnderTest = new ClassUnderTest(mockSF.Object, generator);

            //act
            classUnderTest.DoWork();

            //assert
            foreach (var item in items)
            {
                mockSF.Verify(c=>c.SomeMethod(item), Times.Once());
            }
        }
    }
}

EDIT: Different approaches can be mixed to define a specific expectations, incl. When(), the obsoleted AtMost(), MockBehavior.Strict, Callback, etc.

Again, Moq is not designed to work on large sets, so there is performance penalty. You are still better off using another measures to verify what data will be passed to the mock.

For the example in the OP, here is a simplified setup:

var mockSF = new Mock<ISomeInterface>(MockBehavior.Strict);
var cnt = 0;

mockSF.Setup(m => m.SomeMethod(It.Is<int>(i => i != -1)));
mockSF.Setup(m => m.SomeMethod(It.Is<int>(i => i == 12345))).Callback(() =>cnt++).AtMostOnce();

This will throw for -1, for more than one invocation with 12, and assertion can be made on cnt != 0.



来源:https://stackoverflow.com/questions/16796785/moq-is-slow-to-verify-dependency-after-a-large-number-calls

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!