验证传递给 Mock 的参数是否按预期设置的正确方法

发布于 2024-11-02 07:16:13 字数 3117 浏览 1 评论 0原文

如果您稍后验证调用了这些方法,是否可以在回调中进行断言?这是确保我的模拟获得传递给它的预期参数的首选方法,还是应该在回调中设置局部变量并在该实例上执行断言?

我遇到的情况是,Presenter 类中有一些逻辑,它根据输入派生值并将它们传递给 Creator 类。为了测试 Presenter 类中的逻辑,我想验证在调用 Creator 时是否观察到正确的派生值。我想出了下面的示例,但我不确定我是否喜欢这种方法:

[TestFixture]
public class WidgetCreatorPresenterTester
{
    [Test]
    public void Properly_Generates_DerivedName()
    {
        var widgetCreator = new Mock<IWidgetCreator>();
        widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
                     .Callback((Widget widget) => 
                     Assert.AreEqual("Derived.Name", widget.DerivedName));

        var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
        presenter.Save("Name");

        widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    }
}

我很担心,因为如果最后没有 Verify 调用,就无法保证断言将调用回调。另一种方法是在回调中设置一个局部变量:

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;
    widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

我觉得这种方法更不容易出错,因为它更明确,并且更容易看到 Assert 语句将被调用。一种方法优于另一种方法吗?有没有更简单的方法来测试传递给我缺少的模拟的输入参数?

如果有帮助,这里是此示例的其余代码:

public class Widget
{
    public string Name { get; set; }
    public string DerivedName { get; set; }
}

public class WidgetCreatorPresenter
{
    private readonly IWidgetCreator _creator;

    public WidgetCreatorPresenter(IWidgetCreator creator)
    {
        _creator = creator;
    }

    public void Save(string name)
    {
        _creator.Create(
            new Widget { Name = name, DerivedName = GetDerivedName(name) });
    }

    //This is the method I want to test
    private static string GetDerivedName(string name)
    {
        return string.Format("Derived.{0}", name);
    }
}

public interface IWidgetCreator
{
    void Create(Widget widget);
}

编辑
我更新了代码,以使问题中概述的第二种方法更易于使用。我将设置/验证中使用的表达式的创建拉入一个单独的变量中,因此我只需定义它一次。我觉得这种方法是我最满意的,它很容易设置,并且失败时会显示良好的错误消息。

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;

    Expression<Action<IWidgetCreator>> expressionCreate = 
        (w => w.Create(It.IsAny<Widget>()));
    widgetCreator.Setup(expressionCreate)
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(expressionCreate, Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

Is it acceptable to do asserts in your callbacks if you later verify that the methods were called? Is this the preferred way of making sure my mock is getting the expected parameters passed to it, or should I set a local variable in my callback and do the asserts on that instance?

I have a situation where I have some logic in a Presenter class that derives values based on inputs and passes them to a Creator class. To test the logic in the Presenter class I want to verify that the proper derived values are observed when the Creator is called. I came up with the example below that works, but I'm not sure if I like this approach:

[TestFixture]
public class WidgetCreatorPresenterTester
{
    [Test]
    public void Properly_Generates_DerivedName()
    {
        var widgetCreator = new Mock<IWidgetCreator>();
        widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
                     .Callback((Widget widget) => 
                     Assert.AreEqual("Derived.Name", widget.DerivedName));

        var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
        presenter.Save("Name");

        widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    }
}

I'm concerned because without the Verify call at the end, there is no guarantee that the assert in the callback would be invoked. Another approach would be to set a local variable in the callback:

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;
    widgetCreator.Setup(a => a.Create(It.IsAny<Widget>()))
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(It.IsAny<Widget>()), Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

I feel that this approach is less error prone since it is more explicit, and it's easier to see that the Assert statements will be called. Is one approach preferable to the other? Is there a simpler way to test the input parameter passed to a mock that I'm missing?

In case it is helpful, here is the rest of the code for this example:

public class Widget
{
    public string Name { get; set; }
    public string DerivedName { get; set; }
}

public class WidgetCreatorPresenter
{
    private readonly IWidgetCreator _creator;

    public WidgetCreatorPresenter(IWidgetCreator creator)
    {
        _creator = creator;
    }

    public void Save(string name)
    {
        _creator.Create(
            new Widget { Name = name, DerivedName = GetDerivedName(name) });
    }

    //This is the method I want to test
    private static string GetDerivedName(string name)
    {
        return string.Format("Derived.{0}", name);
    }
}

public interface IWidgetCreator
{
    void Create(Widget widget);
}

EDIT
I updated the code to make the second approach I outlined in the question easier to use. I pulled creation of the expression used in Setup/Verify into a separate variable so I only have to define it once. I feel like this method is what I'm most comfortable with, it's easy to setup and fails with good error messages.

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();
    Widget localWidget = null;

    Expression<Action<IWidgetCreator>> expressionCreate = 
        (w => w.Create(It.IsAny<Widget>()));
    widgetCreator.Setup(expressionCreate)
        .Callback((Widget widget) => localWidget = widget);

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(expressionCreate, Times.Once());
    Assert.IsNotNull(localWidget);
    Assert.AreEqual("Derived.Name", localWidget.DerivedName);
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

变身佩奇 2024-11-09 07:16:13

我所做的就是根据 AAA 的要求进行Verify 匹配。因此不需要安装。您可以内联它,但我将其分开以使其看起来更干净。

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(MatchesWidget("Derived.Name"));
}

private Widget MatchesWidget(string derivedName)
{
    return It.Is<Widget>(m => m.DerivedName == derivedName);
}

What I do is do the Verify with matches in keeping with AAA. And becuase of this the Setup is not required. You can inline it but I separated it out to make it look cleaner.

[Test]
public void Properly_Generates_DerivedName()
{
    var widgetCreator = new Mock<IWidgetCreator>();

    var presenter = new WidgetCreatorPresenter(widgetCreator.Object);
    presenter.Save("Name");

    widgetCreator.Verify(a => a.Create(MatchesWidget("Derived.Name"));
}

private Widget MatchesWidget(string derivedName)
{
    return It.Is<Widget>(m => m.DerivedName == derivedName);
}
ぽ尐不点ル 2024-11-09 07:16:13

由于代码的结构方式,您被迫在一个单元测试中测试两件事。您正在测试 A) 您的演示者正在调用注入的 WidgetCreator 的创建方法,以及 B) 在新的 Widget 上设置了正确的名称。如果可能的话,如果你能以某种方式使这两件事成为两个单独的测试,那就更好了,但在这种情况下,我真的没有找到办法做到这一点。

考虑到所有这些,我认为第二种方法更干净。它更明确地说明了您的期望,如果失败了,那么失败的原因和失败的地方就很清楚了。

Because of the way your code is structured, you're kind of forced to test two things in one unit test. You're testing that A) your presenter is calling the injected WidgetCreator's create method and B) that the correct name is set on the new Widget. If possible, it'd be better if you can somehow make these two things two separate tests, but in this case I don't really see a way to do that.

Given all that, I think the second approach is cleaner. It's more explicit as to what you're expecting, and if it fails, it'd make perfect sense why and where it's failing.

西瑶 2024-11-09 07:16:13

只是为了详细说明 @rsbarro 的评论 - Moq 失败错误消息:

预期对模拟至少调用一次,但从未执行

……对于复杂类型来说,当准确确定哪个条件实际上失败时,当寻找错误时(无论是在代码中还是在代码中),没有什么帮助。单元测试)。

当我使用 Moq Verify 来验证 Verify 中的大量条件时,我经常遇到这种情况,其中必须使用特定参数值调用该方法,这些参数值不是像 Verify 这样的基元。代码>int 或字符串
(对于基元类型来说,这通常不是问题,因为 Moq 列出了方法上实际的“执行的调用”作为异常的一部分)。

因此,在这种情况下,我需要捕获传入的参数(在我看来,这似乎重复了 Moq 的工作),或者只是将断言与 Setup 内联移动代码> / <代码>回调。

例如验证:

widgetCreator.Verify(wc => wc.Create(
      It.Is<Widget>(w => w.DerivedName == "Derived.Name"
                    && w.SomeOtherCondition == true),
      It.Is<AnotherParam>(ap => ap.AnotherCondition == true),
  Times.Exactly(1));

将被重新编码为

widgetCreator.Setup(wc => wc.Create(It.IsAny<Widget>(),
                                    It.IsAny<AnotherParam>())
             .Callback<Widget, AnotherParam>(
              (w, ap) =>
                {
                  Assert.AreEqual("Derived.Name", w.DerivedName);
                  Assert.IsTrue(w.SomeOtherCondition);
                  Assert.IsTrue(ap.AnotherCondition, "Oops");
                });

// *** Act => invoking the method on the CUT goes here

// Assert + Verify - cater for rsbarro's concern that the Callback might not have happened at all
widgetCreator.Verify(wc => wc.Create(It.IsAny<Widget>(), It.Is<AnotherParam>()),
  Times.Exactly(1));

乍一看,这违反了 AAA,因为我们将 AssertArrange 内联(尽管回调仅在调用期间行动),但至少我们可以弄清问题的根源。

另请参阅 Hady 的想法,即将“跟踪”回调 lambda 移至其自己的命名函数中,或者更好的是,在 C#7 中,可以将其移至 本地函数位于单元底部test方法,因此可以保留AAA布局。

Just to elaborate on @rsbarro's comment - the Moq failure error message:

Expected invocation on the mock at least once, but was never performed

... is less than helpful for complex types, when determining exactly which condition actually failed, when hunting down a bug (whether in the code or unit test).

I often encounter this when using Moq Verify to verify a large number of conditions in the Verify, where the method must have been called with specific parameter values which are not primitives like int or string.
(This is not typically a problem for primitive types, since Moq lists the actual "performed invocations" on the method as part of the exception).

As a result, in this instance, I would need to capture the parameters passed in (which to me seems to duplicate the work of Moq), or just move the Assertion inline with Setup / Callbacks.

e.g. the Verification:

widgetCreator.Verify(wc => wc.Create(
      It.Is<Widget>(w => w.DerivedName == "Derived.Name"
                    && w.SomeOtherCondition == true),
      It.Is<AnotherParam>(ap => ap.AnotherCondition == true),
  Times.Exactly(1));

Would be recoded as

widgetCreator.Setup(wc => wc.Create(It.IsAny<Widget>(),
                                    It.IsAny<AnotherParam>())
             .Callback<Widget, AnotherParam>(
              (w, ap) =>
                {
                  Assert.AreEqual("Derived.Name", w.DerivedName);
                  Assert.IsTrue(w.SomeOtherCondition);
                  Assert.IsTrue(ap.AnotherCondition, "Oops");
                });

// *** Act => invoking the method on the CUT goes here

// Assert + Verify - cater for rsbarro's concern that the Callback might not have happened at all
widgetCreator.Verify(wc => wc.Create(It.IsAny<Widget>(), It.Is<AnotherParam>()),
  Times.Exactly(1));

At first glance, this violates AAA, since we are putting the Assert inline with the Arrange (although the callback is only invoked during the Act), but at least we can get to the bottom of the issue.

Also see Hady's idea of moving the 'tracking' callback lambda into its own named function, or better still, in C#7, this can be moved to a Local Function at the bottom of the unit test method, so the AAA layout can be retained.

分開簡單 2024-11-09 07:16:13

基于 StuartLC 在本线程中的回答,您可以按照他的建议进行操作,而不会违反AAA 通过编写传递给 的“内联”函数验证模拟对象的 方法。

例如:

// Arrange
widgetCreator
  .Setup(wc => wc.Create(It.IsAny<Widget>(), It.IsAny<AnotherParam>());

// Act
// Invoke action under test here...

// Assert
Func<Widget, bool> AssertWidget = request =>
{
  Assert.AreEqual("Derived.Name", w.DerivedName);
  Assert.IsTrue(w.SomeOtherCondition);
  Assert.IsTrue(ap.AnotherCondition, "Oops");
  return true;
};

widgetCreator
  .Verify(wc => wc.Create(It.Is<Widget>(w => AssertWidget(w)), It.Is<AnotherParam>()), Times.Exactly(1));

Building on top of StuartLC's answer in this thread, you follow what he is suggesting without violating AAA by writing an "inline" function that is passed to the Verify method of a mock object.

So for example:

// Arrange
widgetCreator
  .Setup(wc => wc.Create(It.IsAny<Widget>(), It.IsAny<AnotherParam>());

// Act
// Invoke action under test here...

// Assert
Func<Widget, bool> AssertWidget = request =>
{
  Assert.AreEqual("Derived.Name", w.DerivedName);
  Assert.IsTrue(w.SomeOtherCondition);
  Assert.IsTrue(ap.AnotherCondition, "Oops");
  return true;
};

widgetCreator
  .Verify(wc => wc.Create(It.Is<Widget>(w => AssertWidget(w)), It.Is<AnotherParam>()), Times.Exactly(1));
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文