使用 Moq 验证呼叫是否按正确的顺序进行
本文关键字:顺序 Moq 验证 呼叫 是否 使用 | 更新日期: 2023-09-27 18:34:09
我需要测试以下方法:
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
,并显示消息">IWriter.Write(( 调用失败,模拟行为严格。模拟上的所有调用都必须具有相应的设置。
我也发现很难找到任何明确的、最新的最小起订量序列文档/示例。
我做错了什么,还是不能使用相同的方法设置序列?如果没有,是否有我可以使用的替代方案(最好使用最小起订量/NUnit(?
在同一模拟上使用 MockSequence 时存在错误。它肯定会在 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)));
我已经设法获得了我想要的行为,但它需要从 http://dpwhelan.com/blog/software-development/moq-sequences/下载第 3 方库
然后可以使用以下内容测试序列:
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 序列扩展会很高兴看到。
我编写了一个扩展方法,该方法将根据调用顺序进行断言。
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 只有一个声明,因此所有闭包都将引用同一变量。使用 expectCallCount,每次循环迭代都会实例化一个新实例(而不是简单地在闭包中使用 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);
}
最近,我为Moq整理了两个功能:VerifyInSequence((和VerifyNotInSequence((。他们甚至可以与松散的模拟一起工作。但是,这些仅在最小起订量存储库分支中可用:
https://github.com/grzesiek-galezowski/moq4
并等待更多评论和测试,然后再决定是否可以将它们包含在官方最小起订量发布中。但是,没有什么能阻止您将源代码下载为 ZIP,将其构建到 dll 中并尝试一下。使用这些功能,您需要的序列验证可以这样编写:
var mockWriter = new Mock(( { CallSequence = new LooseSequence(( };执行必要的调用mockWriter.VerifyInSequence(x => x.Write(expectType((;mockWriter.VerifyInSequence(x => x.Write(expectId((;mockWriter.VerifyInSequence(x => x.Write(expectSender((;
(请注意,您可以根据需要使用其他两个序列。松散序列将允许要验证的调用之间的任何调用。StrictSequence 不允许这样做,而 StrictAnytimeSequence 就像 StrictSequence(经过验证的调用之间没有方法调用(,但允许序列前面有任意数量的任意调用。
如果您决定尝试此实验性功能,请评论您的想法:https://github.com/Moq/moq4/issues/21
谢谢!
最简单的解决方案是使用队列:
var expectedParameters = new Queue<string>(new[]{expectedType,expectedId,expectedSender});
mockWriter.Setup(x => x.Write(expectedType))
.Callback((string s) => Assert.AreEqual(expectedParameters.Dequeue(), s));
我刚刚遇到了类似的场景,并且受到接受答案的启发,我使用了以下方法:
//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);
如果您有不同类型的重载,您也可以对重载运行相同的设置。
我的方案是没有参数的方法:
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"]
我怀疑预期的 Id 不是你所期望的。
但是,在这种情况下,我可能只是编写自己的IWriter实现来验证...可能容易得多(以后更容易更改(。
很抱歉没有直接的最小起订量建议。我喜欢它,但没有这样做。
您是否可能需要添加.验证(( 在每个设置结束时?(这确实是一个猜测,尽管我担心(。
我迟到了,但我想分享一个对我有用的解决方案,因为似乎所有引用的解决方案都不适用于多次验证相同的方法调用(使用相同的参数(。此外,引用的错误, Moq 问题 #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! ");
}
}