【问题标题】:Verifying a method was called验证方法被调用
【发布时间】:2009-12-30 13:04:41
【问题描述】:

使用起订量时,我遇到了一个非常奇怪的问题,即只有当我设置的方法是公开的时,模拟上的设置似乎才有效。我不知道这是 Moq 错误还是我只是有这个错误(Moq 的新手)。这是测试用例:

public class TestClass
{
    public string Say()
    {
        return Hello();
    }

    internal virtual string Hello()
    {
        return "";
    }
}

[TestMethod]
public void Say_WhenPublic_CallsHello()
{
    Mock<TestClass> mock = new Mock<TestClass>();
    mock.Setup(x => x.Hello()).Returns("Hello World");

    string result = mock.Object.Say();
    mock.Verify(x => x.Hello(), Times.Exactly(1));
    Assert.AreEqual("Hello World", result);     
}

失败并显示此消息:

Say_WhenPublic_CallsHello 失败:Moq.MockException: 未在模拟上执行 1 次调用:x => x.Hello() 在 Moq.Mock.ThrowVerifyException(预期 IProxyCall,表达式表达式,Times 次)...

如果我像这样公开 Hello 方法,则测试通过。这里有什么问题?

public virtual string Hello()
{
    return "";
}

提前致谢!

【问题讨论】:

  • 投票否决,因为当前版本的 Moq 中不再存在该行为。它现在能够处理内部的非虚拟属性和方法。
  • 与其投票否决(以及任何相关的答案),不如您提供引入此功能的最小起订量版本号,我们将进行相应的编辑。投票反对并没有真正实现任何目标。 Q 和 A 对于没有此功能的 Moq 版本仍然有效。

标签: c# .net mocking moq verify


【解决方案1】:

Hello() 是内部时,测试失败,因为在这种情况下,Moq 无法提供对该方法的覆盖。这意味着 Hello() 的内部实现将运行,而不是 mock 的版本,导致 Verify() 失败。

顺便说一句,在单元测试的上下文中,您在此处执行的操作毫无意义。单元测试不应该关心Say() 调用内部Hello() 方法。这是程序集内部的实现,与使用代码无关。

【讨论】:

  • +1 此外,单元测试应该在与被测系统不同的库中编写。如果这样做了,代码甚至都不会编译。
  • @Mark - true,尽管使用 InternalVisibleTo 属性可以使内部组件对其他程序集可见
  • @Mark - 这是一个显而易见的声明 - 这意味着您当然应该在不同的库中编写测试。此代码旨在提供一个简单的示例。为什么我要付出额外的努力将这两个分成不同的类来说明一个简单的观点?
  • @AdamRalph - 你没听说过部分模拟吗?这就是这个测试用例(以及我的整个帖子)的内容。在部分模拟中,您可以仅部分模拟被测主题,这意味着您可以在模拟对象上调用真实方法,并对被测主题中的其他方法进行模拟设置。这在为原本无法测试的遗留代码编写测试时非常有价值。考虑一个有几个方法的遗留类,其中一些在内部调用数据库。进一步考虑您可能只想模拟调用数据库的方法。
  • @jle - 我对此没有异议,我同意部分模拟可能非常有用。我不明白将测试目标与内部 Hello() 调用隔离开来的动机。也许您的示例没有传达有关您的测试目标以及内部调用在具体实现中的作用的足够信息
【解决方案2】:

我不太了解这在幕后是如何工作的,因此无法为您提供技术性答案,说明为什么会出现这种行为,但我想我可以帮助您解决困惑。

在您的示例中,您正在调用方法 Say(),它正在返回预期的文本。您的期望应该强制执行 Say() 的特定实现,即它调用名为 Hello() 的内部方法来返回该字符串。这就是为什么它没有通过验证的原因,也是为什么返回的字符串是“”,即Hello()的实际实现已经被调用了。

通过公开 Hello 方法,看起来这使 Moq 能够拦截对其的调用,并改为使用它的实现。因此,在这种情况下,测试似乎通过了。但是,在这种情况下,您并没有真正实现任何有用的东西,因为您的测试表明,当您调用 Say() 时,结果是“Hello World”,而实际上结果是“”。

我已经重写了您的示例,以展示我希望如何使用 Moq(不一定是确定的,但希望清楚。

public interface IHelloProvider
{
    string Hello();
}

public class TestClass
{
    private readonly IHelloProvider _provider;

    public TestClass(IHelloProvider provider)
    {
        _provider = provider;
    }

    public string Say()
    {
        return _provider.Hello();
    }
}

[TestMethod]
public void WhenSayCallsHelloProviderAndReturnsResult()
{
    //Given
    Mock<IHelloProvider> mock = new Mock<IHelloProvider>();
    TestClass concrete = new TestClass(mock.Object);
    //Expect
    mock.Setup(x => x.Hello()).Returns("Hello World");
    //When
    string result = concrete.Say();
    //Then
    mock.Verify(x => x.Hello(), Times.Exactly(1));
    Assert.AreEqual("Hello World", result);
}

在我的示例中,我引入了 IHelloProvider 的接口。您会注意到没有 IHelloProvider 的实现。这是我们试图通过使用模拟解决方案来实现的核心。

我们正在尝试测试一个依赖于外部事物 (IHelloProvider) 的类 (TestClass)。如果您正在使用测试驱动开发,那么您可能还没有编写 IHelloProvider,但您知道在某些时候您将需要一个。不过,您希望首先让 TestClass 工作,而不是分心。或者可能 IHelloProvider 使用数据库,或平面文件,或者难以配置。

即使你有一个完全工作的 IHelloProvider,你仍然只是试图测试 TestClass 的行为,所以使用一个具体的 HelloProvider 可能会使你的测试更容易失败,例如如果行为发生了变化HelloProvider,你不想改变每个使用它的类的测试,你只想改变 HelloProvider 测试。

回到代码,我们现在有一个类TestClass,它依赖于一个接口IHelloProvider,它的实现是在构造时提供的(这就是依赖注入)。

Say() 的行为是它调用 IHelloProvider 上的方法 Hello()。

如果您回顾一下测试,我们已经创建了一个实际的 TestClass 对象,因为我们实际上想要测试我们编写的代码。我们创建了一个 Mock IHelloProvider,并表示我们希望它调用它的 Hello() 方法,并在它调用时返回字符串“Hello World”。

然后我们调用 Say(),并像以前一样验证结果。

要意识到的重要一点是,我们对 IHelloProvider 的行为不感兴趣,因此我们可以模拟它以简化测试。我们对 TestClass 的行为感兴趣,所以我们创建了一个实际的 TestClass 而不是 Mock,以便我们可以测试它的实际行为。

我希望这有助于澄清发生了什么。

【讨论】:

  • +1 - 要记住的一个重要概念(正如您所指出的)是您不会模拟被测类,而是模拟它的依赖项。
  • @Modan - 感谢您花这么多时间回复您。然而,我应该在我的帖子中更清楚地说明我的最终目标。我正在探索使用 Moq 进行部分模拟的概念,并且对为什么对类的内部方法进行设置不起作用感到困惑。你看,我正在对一些遗留代码进行一些更改,而我完全更改此代码的能力是有限的。我认为我能做的只是模拟我的被测对象的方法,这些方法不能在单元测试中执行(例如访问数据库)。再次感谢。
  • “我认为我能做的只是模拟我的被测对象的方法,这些方法不能在单元测试中执行(例如访问数据库”在这种情况下,我建议拆分该代码像我在我的示例中所做的那样进入一个单独的类。将无法轻松测试的代码与可测试的代码分开是有用的(甚至可能是必要的)。如果你不这样做,你很可能会结束由于您的代码结构,一些难以/不可能测试的业务逻辑。
【解决方案3】:

Moq 不做部分模拟,只能模拟公共虚拟方法或接口。创建 Mock 时,您将创建一个全新的 T 并删除任何公共虚拟方法的所有实现。

我的建议是专注于测试对象的公共表面积,而不是它们的内部。您的内部将得到覆盖。只需确保您清楚了解您的目标类是什么,并且不要模拟它(在大多数情况下)。

部分模拟仅在您想要使用抽象方法的模拟实现来测试抽象类的功能时才有用。。如果您不这样做,您可能不会从进行部分模拟中看到太多好处

如果这不是一个抽象类,你会想要更多地关注Modan建议的方法,也就是说你应该模拟依赖而不是你的目标类本身。所有这一切都归结为规则不要模拟您正在测试的内容

【讨论】:

  • 这不再是真的。 Moq 现在能够执行非虚拟的内部方法。
猜你喜欢
  • 2011-12-25
  • 1970-01-01
  • 1970-01-01
  • 2018-08-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多