【问题标题】:Using Moq to verify calls are made in the correct order使用 Moq 来验证调用是否以正确的顺序进行
【发布时间】:2012-05-23 01:19:14
【问题描述】:

我需要测试以下方法:

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

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

我创建了一个最小起订量的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() 调用失败,模拟行为严格。所有模拟上的调用必须有相应的设置。"。

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

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

【问题讨论】:

标签: c# unit-testing nunit moq sequential


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    我刚刚遇到过类似的情况,并且受到公认答案的启发,我使用了以下方法:

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

    【讨论】:

      【解决方案3】:

      我迟到了,但我想分享一个对我有用的解决方案,因为似乎所有引用的解决方案都无法验证相同 按顺序多次调用方法(使用相同的参数)。此外,引用的错误,Moq Issue #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! ");
          }
      }
      

      【讨论】:

        【解决方案4】:

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

        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"]
        

        【讨论】:

          【解决方案5】:

          using MockSequence on same mock 时有错误。它肯定会在 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的发布!最好的测试工具之一是......
          • 这里警告一句,如果Write() 方法从未被调用,这些回调将没有机会断言任何东西。确保添加一个 catch all 验证该方法至少被调用一次。
          • github 上正在进行关于测序的讨论,任何想要贡献/评论的人:github.com/moq/moq4/issues/75
          • 这是个好主意,谢谢!我实际上更喜欢这个,因为这意味着我可以覆盖我之前的设置。我确实在每个设置中添加了.Verifiable,并在最后添加了.Verify(),以检查它们是否实际运行,以及是否以正确的顺序运行:)
          【解决方案6】:

          最简单的解决方案是使用Queue

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

          【讨论】:

            【解决方案7】:

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

            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 只有一个声明,所有的闭包都会引用同一个变量。使用 expectedCallCount,循环的每次迭代都会实例化一个新实例(而不是简单地在闭包中使用 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);
            }
            

            【讨论】:

              【解决方案8】:

              最近,我为 Moq 整合了两个功能:VerifyInSequence() 和 VerifyNotInSequence()。他们甚至可以使用 Loose Mocks。但是,这些仅在最小起订量存储库分支中可用:

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

              并等待更多的 cmets 和测试,然后再决定是否可以将它们包含在官方最小起订量中。但是,没有什么能阻止您以 ZIP 格式下载源代码,将其构建为 dll 并试一试。使用这些特性,你需要的序列验证可以这样写:

              var mockWriter = new Mock() { CallSequence = new LooseSequence() }; //执行必要的调用 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

              谢谢!

              【讨论】:

                【解决方案9】:

                我已经设法获得我想要的行为,但它需要从 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-sequences 扩展会很高兴。

                【讨论】:

                • 很遗憾你不能下午。酷不知道存在:D。但是,一旦你进入这个领域,你就真的很讨厌,这也许是这个地区有点缺乏起订量的原因——是的,这个项目看起来很安静。看着它的问题和排序已经在那里突出了 4 年。我还注意到最新版本的名称中确实包含 final。
                【解决方案10】:

                我怀疑 expectedId 不是您所期望的。

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

                很抱歉没有直接起订量建议。我喜欢它,但还没有这样做。

                您可能需要在每次设置结束时添加 .Verify() 吗? (虽然我很害怕,但这确实是一个猜测)。

                【讨论】:

                • 我已经仔细检查过,值正确且符合预期。滚动我自己的模拟是我现在尝试避免的事情,我现在使用 Moq,但可能不得不求助于!并且在设置后无法添加验证(),只有 Verifiable() - 遗憾的是没有区别。不过感谢您的指点。
                猜你喜欢
                • 1970-01-01
                • 2016-06-12
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2014-07-24
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多