问题
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