简体   繁体   English

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

[英]Using Moq to verify calls are made in the correct order

I need to test the following method:我需要测试以下方法:

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

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

I've created a Moq'd IWriter and I want to ensure that the Write() methods are called in the right order.我已经创建了一个 Moq'd IWriter ,我想确保Write()方法以正确的顺序调用。

I have the following test code:我有以下测试代码:

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

However, the second call to Write() in CreateOutput() (to write the id value) throws a MockException with the message " IWriter.Write() invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup. ".但是,在CreateOutput()Write()的第二次调用(写入id值)抛出MockException并带有消息“ IWriter.Write() 调用失败,模拟行为严格。模拟上的所有调用都必须有相应的设置。 ”。

I'm also finding it hard to find any definitive, up-to-date documentation/examples of Moq sequences.我还发现很难找到任何明确的、最新的 Moq 序列文档/示例。

Am I doing something wrong, or can I not set up a sequence using the same method?我做错了什么,或者我不能使用相同的方法设置序列? If not, is there an alternative I can use (preferably using Moq/NUnit)?如果没有,有没有我可以使用的替代方法(最好使用 Moq/NUnit)?

There is bug when using MockSequence on same mock .在同一个 mock 上使用 MockSequence时存在错误。 It definitely will be fixed in later releases of Moq library (you can also fix it manually by changing Moq.MethodCall.Matches implementation).它肯定会在 Moq 库的后续版本中修复(您也可以通过更改Moq.MethodCall.Matches实现来手动修复它)。

If you want to use Moq only, then you can verify method call order via callbacks:如果您只想使用 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)));

I've managed to get the behaviour I want, but it requires downloading a 3rd-party library from http://dpwhelan.com/blog/software-development/moq-sequences/我设法得到了我想要的行为,但它需要从http://dpwhelan.com/blog/software-development/moq-sequences/下载一个 3rd-party 库

The sequence can then be tested using the following:然后可以使用以下方法测试序列:

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

I've added this as an answer partly to help document this solution, but I'm still interested in whether something similar could be achieved using Moq 4.0 alone.我添加了这个作为答案,部分是为了帮助记录这个解决方案,但我仍然对是否可以单独使用 Moq 4.0 实现类似的东西感兴趣。

I'm not sure if Moq is still in development, but fixing the problem with the MockSequence , or including the moq-sequences extension in Moq would be good to see.我不确定 Moq 是否仍在开发中,但是解决MockSequence的问题,或者在 Moq 中包含 moq-sequences 扩展会很高兴看到。

I wrote an extension method that will assert based on order of invocation.我编写了一个扩展方法,它将根据调用顺序进行断言。

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

It works by taking advantage of the way that closures work with respect to scoped variables.它的工作原理是利用闭包对作用域变量的工作方式。 Since there is only one declaration for sharedCallCount, all of the closures will have a reference to the same variable.因为只有一个 sharedCallCount 声明,所以所有的闭包都将引用同一个变量。 With expectedCallCount, a new instance is instantiated each iteration of the loop (as opposed to simply using i in the closure).使用 expectedCallCount,每次循环迭代都会实例化一个新实例(而不是简单地在闭包中使用 i)。 This way, each closure has a copy of i scoped only to itself to compare with the sharedCallCount when the expressions are invoked.这样,每个闭包都有一个 i 的副本,范围仅限于它自己,以便在调用表达式时与 sharedCallCount 进行比较。

Here's a small unit test for the extension.这是扩展的一个小单元测试。 Note that this method is called in your setup section, not your assertion section.请注意,此方法是在您的设置部分而不是您的断言部分中调用的。

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

The simplest solution would be using a Queue :最简单的解决方案是使用Queue

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

Recently, I put together two features for Moq: VerifyInSequence() and VerifyNotInSequence().最近,我为 Moq 组合了两个特性:VerifyInSequence() 和 VerifyNotInSequence()。 They work even with Loose Mocks.他们甚至可以使用 Loose Mocks。 However, these are only available in a moq repository fork:但是,这些仅在 moq 存储库分支中可用:

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

and await more comments and testing before deciding on whether they can be included in official moq releaase.并等待更多评论和测试,然后再决定它们是否可以包含在官方最小起订量发布中。 However, nothing prevents you from downloading the source as ZIP, building it into a dll and giving it a try.但是,没有什么可以阻止您以 ZIP 格式下载源代码,将其构建到 dll 中并尝试一下。 Using these features, the sequence verification you need could be written as such:使用这些功能,您需要的序列验证可以写成这样:

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

(note that you can use two other sequences, depending on your needs. Loose sequence will allow any calls between the ones you want to verify. StrictSequence will not allow this and StrictAnytimeSequence is like StrictSequence (no method calls between verified calls), but allows the sequence to be preceeded by any number of arbitrary calls. (请注意,您可以使用其他两个序列,具体取决于您的需要。松散序列将允许您要验证的序列之间的任何调用。StrictSequence 不允许这样做,而 StrictAnytimeSequence 类似于 StrictSequence(已验证调用之间没有方法调用),但允许任意数量的任意调用之前的序列。

If you decide to give this experimental feature a try, please comment with your thoughts on: https://github.com/Moq/moq4/issues/21如果您决定尝试此实验性功能,请在以下位置发表您的想法: https : //github.com/Moq/moq4/issues/21

Thanks!谢谢!

I've just had a similar scenario, and inspired by the accepted answer, I've used the following approach:我刚刚遇到了类似的情况,并受到公认答案的启发,我使用了以下方法:

//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);

I suspect that expectedId is not what you expect.我怀疑 expectedId 不是你所期望的。

However i'd probably just write my own implementation of IWriter to verify in this case ... probably a lot easier (and easier to change later).但是,在这种情况下,我可能只是编写自己的 IWriter 实现来验证……可能会容易得多(并且以后更容易更改)。

Sorry for no Moq advice directly.很抱歉没有直接提供起订量建议。 I love it, but haven't done this in it.我喜欢它,但还没有这样做。

do you maybe need to add .Verify() at the end of each setup?您可能需要在每次设置结束时添加 .Verify() 吗? (That really is a guess though i'm afraid). (这真的是一个猜测,虽然我很害怕)。

My scenario was methods without parameters:我的场景是没有参数的方法:

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

So I used Invocations property on the Mock to compare what was called:所以我在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),
        });

You could also append the invocation.Arguments to check method calls with parameters.您还可以附加invocation.Arguments以检查带参数的方法调用。

Also the failure message is more clear than just expected 1 but was 5 :此外,失败消息比expected 1 but was 5更清楚expected 1 but was 5

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

I am late to this party but I wanted to share a solution that worked for me since it seems as though all of the referenced solutions did not work with verifying the same method call (with the same arguments) multiple times in order.我迟到了,但我想分享一个对我有用的解决方案,因为似乎所有引用的解决方案都无法按顺序多次验证相同的方法调用(使用相同的参数)。 In addition the referenced bug, Moq Issue #478 was closed without a solution.此外,引用的错误Moq Issue #478已在没有解决方案的情况下关闭。

The solution presented utilizes the MockObject.Invocations list to determine order and sameness.提出的解决方案利用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! ");
    }
}

Moq has a little-known feature called Capture.In , which can capture arguments passed to a method. Moq有一个鲜为人知的功能,称为Capture.In ,它可以捕获传递给方法的参数。 With it, you can verify call order like this:有了它,您可以像这样验证调用顺序:

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

CollectionAssert.AreEqual(calls, expectedCalls);

If you have overloads with different types, you can run the same setup for overloads too.如果您有不同类型的重载,您也可以为重载运行相同的设置。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM