使用 Moq 验证调用的顺序是否正确

我需要测试以下方法:

CreateOutput(IWriter writer)
{
writer.Write(type);
writer.Write(id);
writer.Write(sender);


// many more Write()s...
}

我已经创建了一个 Moq’d IWriter,我想确保 Write()方法的调用顺序是正确的。

我有以下测试代码:

var mockWriter = new Mock<IWriter>(MockBehavior.Strict);
var sequence = new MockSequence();
mockWriter.InSequence(sequence).Setup(x => x.Write(expectedType));
mockWriter.InSequence(sequence).Setup(x => x.Write(expectedId));
mockWriter.InSequence(sequence).Setup(x => x.Write(expectedSender));

但是,在 CreateOutput()中第二次调用 Write()(写入 id值)将抛出一个带有消息“ 因为模拟行为严格,写()调用失败。模拟上的所有调用都必须有相应的设置。”的 MockException

我还发现很难找到任何明确的、最新的 Moq 序列文档/示例。

是我做错了什么,还是我不能用同样的方法设置序列? 如果没有,是否有其他选择(最好使用 Moq/NUnit) ?

38351 次浏览

我怀疑预期的 ID 不是你所期望的。

然而,在这种情况下,我可能只是编写我自己的 IWriter 实现来验证... ... 可能会更容易(以后也更容易更改)。

很抱歉没有直接的建议。我喜欢它,但没有做到这一点在它。

你是不是需要加上。在每个设置结束时验证() ?(恐怕这只是个猜测)。

我已经设法得到我想要的行为,但它需要从 http://dpwhelan.com/blog/software-development/moq-sequences/下载第三方库

然后可以使用以下方法测试序列:

var mockWriter = new Mock<IWriter>(MockBehavior.Strict);
using (Sequence.Create())
{
mockWriter.Setup(x => x.Write(expectedType)).InSequence();
mockWriter.Setup(x => x.Write(expectedId)).InSequence();
mockWriter.Setup(x => x.Write(expectedSender)).InSequence();
}

我添加这个作为一个答案,部分是为了帮助记录这个解决方案,但是我仍然感兴趣的是,是否可以单独使用 Moq 4.0来实现类似的功能。

我不确定 Moq 是否还在开发中,但是修复 MockSequence的问题,或者在 Moq 中包含 Moq 序列扩展将是一件好事。

在相同的模拟上使用模拟序列。它肯定会在 Moq 库的后续版本中得到修复(您也可以通过更改 Moq.MethodCall.Matches实现手动修复它)。

如果只想使用 Moq,那么可以通过回调验证方法调用顺序:

int callOrder = 0;
writerMock.Setup(x => x.Write(expectedType)).Callback(() => Assert.That(callOrder++, Is.EqualTo(0)));
writerMock.Setup(x => x.Write(expectedId)).Callback(() => Assert.That(callOrder++, Is.EqualTo(1)));
writerMock.Setup(x => x.Write(expectedSender)).Callback(() => Assert.That(callOrder++, Is.EqualTo(2)));

最近,我为 Moq 整合了两个特性: VerifyInSequence ()和 VerifyNotInSequence ()。他们甚至可以和 Loose Mocks 一起工作。但是,这些只能在 moq 存储库 fork 中使用:

Https://github.com/grzesiek-galezowski/moq4

并等待更多的评论和测试,然后再决定是否可以将其纳入官方的 moq 发布。但是,没有什么可以阻止您以 ZIP 格式下载源代码,将其构建到 dll 中并尝试使用它。使用这些特性,您需要的序列验证可以这样编写:

var mockWriter = new Mock<IWriter>() { CallSequence = new LooseSequence() };


//perform the necessary calls


mockWriter.VerifyInSequence(x => x.Write(expectedType));
mockWriter.VerifyInSequence(x => x.Write(expectedId));
mockWriter.VerifyInSequence(x => x.Write(expectedSender));

(注意,根据您的需要,您可以使用其他两个序列。松散序列将允许您想要验证的对象之间的任何调用。StrictSequence 不允许这样做,StrictAnytimeSequence 类似于 StrictSequence (在经过验证的调用之间没有方法调用) ,但是允许任意数量的调用在序列之前。

如果您决定尝试这个实验性功能,请评论您的想法: Https://github.com/moq/moq4/issues/21

谢谢!

我编写了一个扩展方法,它将根据调用的顺序进行断言。

public static class MockExtensions
{
public static void ExpectsInOrder<T>(this Mock<T> mock, params Expression<Action<T>>[] expressions) where T : class
{
// All closures have the same instance of sharedCallCount
var sharedCallCount = 0;
for (var i = 0; i < expressions.Length; i++)
{
// Each closure has it's own instance of expectedCallCount
var expectedCallCount = i;
mock.Setup(expressions[i]).Callback(
() =>
{
Assert.AreEqual(expectedCallCount, sharedCallCount);
sharedCallCount++;
});
}
}
}

它利用了闭包在作用域变量方面的工作方式。由于 sharedCallCount 只有一个声明,所有闭包都将引用同一个变量。使用 pectedCallCount,循环的每次迭代都会实例化一个新实例(而不是仅仅在闭包中使用 i)。这样,在调用表达式时,每个闭包都有一个仅限于自身的 i 作用域副本,以便与 sharedCallCount 进行比较。

下面是扩展的一个小单元测试。请注意,这个方法是在设置部分调用的,而不是在断言部分。

[TestFixture]
public class MockExtensionsTest
{
[TestCase]
{
// Setup
var mock = new Mock<IAmAnInterface>();
mock.ExpectsInOrder(
x => x.MyMethod("1"),
x => x.MyMethod("2"));


// Fake the object being called in order
mock.Object.MyMethod("1");
mock.Object.MyMethod("2");
}


[TestCase]
{
// Setup
var mock = new Mock<IAmAnInterface>();
mock.ExpectsInOrder(
x => x.MyMethod("1"),
x => x.MyMethod("2"));


// Fake the object being called out of order
Assert.Throws<AssertionException>(() => mock.Object.MyMethod("2"));
}
}


public interface IAmAnInterface
{
void MyMethod(string param);
}

最简单的解决方案是使用 排队:

var expectedParameters = new Queue<string>(new[]{expectedType,expectedId,expectedSender});
mockWriter.Setup(x => x.Write(expectedType))
.Callback((string s) => Assert.AreEqual(expectedParameters.Dequeue(), s));

我的场景是没有参数的方法:

public interface IWriter
{
void WriteA ();
void WriteB ();
void WriteC ();
}

因此,我使用 Mock上的 Invocations属性来比较所谓的:

var writer = new Mock<IWriter> ();


new SUT (writer.Object).Run ();


Assert.Equal (
writer.Invocations.Select (invocation => invocation.Method.Name),
new[]
{
nameof (IWriter.WriteB),
nameof (IWriter.WriteA),
nameof (IWriter.WriteC),
});

您还可以附加 invocation.Arguments来检查带有参数的方法调用。

此外,失败的信息比 expected 1 but was 5更加明确:

    expected
["WriteB", "WriteA", "WriteC"]
but was
["WriteA", "WriteB"]

我来晚了,但是我想分享一个对我有用的解决方案,因为似乎所有引用的解决方案都不能按顺序多次验证 < strong > same 方法调用(使用相同的参数)。此外,引用的错误,莫克期刊 # 478被关闭没有解决方案。

所提出的解决方案利用 MockObject.Invocations列表来确定顺序和相同性。

public static void VerifyInvocations<T>(this Mock<T> mock, params Expression<Action<T>>[] expressions) where T : class
{
Assert.AreEqual(mock.Invocations.Count, expressions.Length,
$"Number of invocations did not match expected expressions! Actual invocations: {Environment.NewLine}" +
$"{string.Join(Environment.NewLine, mock.Invocations.Select(i => i.Method.Name))}");


for (int c = 0; c < mock.Invocations.Count; c++)
{
IInvocation expected = mock.Invocations[c];
MethodCallExpression actual = expressions[c].Body as MethodCallExpression;


// Verify that the same methods were invoked
Assert.AreEqual(expected.Method, actual.Method, $"Did not invoke the expected method at call {c + 1}!");


// Verify that the method was invoked with the correct arguments
CollectionAssert.AreEqual(expected.Arguments.ToList(),
actual.Arguments
.Select(arg =>
{
// Expressions treat the Argument property as an Expression, do this to invoke the getter and get the actual value.
UnaryExpression objectMember = Expression.Convert(arg, typeof(object));
Expression<Func<object>> getterLambda = Expression.Lambda<Func<object>>(objectMember);
Func<object> objectValueGetter = getterLambda.Compile();
return objectValueGetter();
})
.ToList(),
$"Did not invoke step {c + 1} method '{expected.Method.Name}' with the correct arguments! ");
}
}

我刚刚经历了一个类似的场景,并受到公认答案的启发,我使用了以下方法:

//arrange
var someServiceToTest = new SomeService();


var expectedCallOrder = new List<string>
{
"WriteA",
"WriteB",
"WriteC"
};
var actualCallOrder = new List<string>();


var mockWriter = new Mock<IWriter>();
mockWriter.Setup(x => x.Write("A")).Callback(() => { actualCallOrder.Add("WriteA"); });
mockWriter.Setup(x => x.Write("B")).Callback(() => { actualCallOrder.Add("WriteB"); });
mockWriter.Setup(x => x.Write("C")).Callback(() => { actualCallOrder.Add("WriteC"); });


//act
someServiceToTest.CreateOutput(_mockWriter.Object);


//assert
Assert.AreEqual(expectedCallOrder, actualCallOrder);

Moq有一个鲜为人知的特性 Capture.In,它可以捕获传递给方法的参数。使用它,您可以像下面这样验证呼叫顺序:

var calls = new List<string>();
var mockWriter = new Mock<IWriter>();
mockWriter.Setup(x => x.Write(Capture.In(calls)));


CollectionAssert.AreEqual(calls, expectedCalls);

如果有不同类型的重载,也可以对重载运行相同的设置。