69

我需要测试以下方法:

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

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

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

我做错了什么,或者我不能使用相同的方法设置序列吗?如果没有,有没有我可以使用的替代方案(最好使用 Moq/NUnit)?

4

10 回答 10

76

在同一 mock 上使用 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)));
于 2012-05-15T22:08:46.037 回答
11

我已经设法得到我想要的行为,但它需要从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 扩展来解决问题会很高兴。

于 2012-05-15T14:33:15.833 回答
10

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

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);
}
于 2013-10-31T16:11:35.737 回答
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));
于 2016-09-25T23:25:41.520 回答
5

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

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

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

var mockWriter = new Mock<IWriter>() { 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

谢谢!

于 2012-12-29T16:59:58.190 回答
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);
于 2020-12-31T15:20:22.047 回答
2

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

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

于 2021-01-28T13:33:00.953 回答
0

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

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

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

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

于 2012-05-15T14:03:55.000 回答
0

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

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

所以我使用Invocations属性Mock来比较所谓的:

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"]
于 2020-05-29T05:18:34.303 回答
0

我参加这个聚会迟到了,但我想分享一个对我有用的解决方案,因为似乎所有引用的解决方案都无法按顺序多次验证相同的方法调用(使用相同的参数)。除了引用的错误之外,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! ");
    }
}
于 2020-09-28T14:02:07.397 回答