使用 Moq 模拟类时,如何仅针对特定方法进行 CallBase?

Posted

技术标签:

【中文标题】使用 Moq 模拟类时,如何仅针对特定方法进行 CallBase?【英文标题】:When mocking a class with Moq, how can I CallBase for just specific methods? 【发布时间】:2011-02-18 20:37:18 【问题描述】:

我非常感谢 Moq 的 Loose 模拟行为,它在未设置期望时返回默认值。它既方便又节省了我的代码,而且还起到了安全措施的作用:在单元测试期间不会无意中调用依赖项(只要它们是虚拟的)。

但是,当被测方法恰好是虚拟方法时,我对如何保持这些好处感到困惑。 在这种情况下,我确实想调用那个方法的真实代码,同时仍然松散地模拟类的其余部分。

我在搜索中发现的只是我可以设置mock.CallBase = true 以确保调用该方法。但是,这会影响整个班级。我不想这样做,因为它让我对隐藏调用依赖关系的类中的所有其他属性和方法进退两难:如果 CallBase 为真,那么我必须要么

    为隐藏依赖关系的所有属性和方法设置存根 -- 即使我的测试认为它不需要关心这些依赖关系,或者 希望我不要忘记设置任何存根(并且以后不会将新的依赖项添加到代码中)- 单元测试遇到真正的依赖项的风险。

我想我想要的是:mock.Setup(m => m.VirtualMethod()).CallBase(); 这样当我调用mock.Object.VirtualMethod() 时,Moq 就会调用真正的实现...

问:使用 Moq,当我模拟类以存根几个依赖项时,有什么方法可以测试虚拟方法? IE。无需诉诸 CallBase=true 并且不必存根所有依赖项?


示例代码说明(使用 MSTest、InternalsVisibleTo DynamicProxyGenAssembly2)

在以下示例中,TestNonVirtualMethod 通过,但 TestVirtualMethod 失败 - 返回 null。

public class Foo

    public string NonVirtualMethod()  return GetDependencyA(); 
    public virtual string VirtualMethod()  return GetDependencyA();

    internal virtual string GetDependencyA()  return "! Hit REAL Dependency A !"; 
    // [... Possibly many other dependencies ...]
    internal virtual string GetDependencyN()  return "! Hit REAL Dependency N !"; 


[TestClass]
public class UnitTest1

    [TestMethod]
    public void TestNonVirtualMethod()
    
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);

        string result = mockFoo.Object.NonVirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    

    [TestMethod]
    public void TestVirtualMethod() // Fails
    
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);
        // (I don't want to setup GetDependencyB ... GetDependencyN here)

        string result = mockFoo.Object.VirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    

    string expectedResultString = "Hit mock dependency A - OK";

【问题讨论】:

只是简要说明您/我的需求:在模拟时需要 CallBase 版本,而不是 “如果没有期望覆盖成员,则调用基类实现”,即所谓的Partial Mocks,请调用mocked members的基础实现!我想将新函数命名为CallBaseOnMockedMemberOnly :D 【参考方案1】:

由于这个问题已经很久没有人回答了,我认为它值得回答,所以我将专注于您提出的***别的问题:当被测方法恰好是虚拟的时,如何保持这些好处。

快速回答:你不能用最小起订量来做,或者至少不能直接做。但是,你可以做到。

假设您有两个行为方面,其中方面 A 是虚拟的,而方面 B 不是。这几乎反映了你在课堂上学到的东西。 B 是否可以使用其他方法;这取决于你。

目前,您的班级 Foo 正在做两件事 - A 和 B。我可以看出它们是不同的职责,只是因为您想模拟 A 并自行测试 B。

您可以:

将行为 A 移到单独的类中 依赖通过Foo的构造函数将带有A的新类注入Foo 从 B 调用该类。

现在您可以模拟 A,并且仍然调用 B..N 的真实代码,而无需实际调用真实的 A。您可以保持 A 虚拟或通过接口访问它并模拟它。这也符合单一职责原则。

您可以使用 Foo 级联构造函数 - 使构造函数 Foo() 调用构造函数 Foo(new A()) - 因此您甚至不需要依赖注入框架来执行此操作。

希望这会有所帮助!

【讨论】:

只是为了检查:您确认如果被测方法是虚拟的,并且我们正在使用 Mock,我们是否必须将 CallBase 设置为 true? 我的建议是,如果您首先调用 CallBase,则可能有不同的方式来获得相同的好处,这可能会导致代码设计在大多数情况下都有效。也许您有绝对需要 CallBase 的罕见上下文之一 - 我不知道。你能告诉我更多关于为什么一个方法的 CallBase 是解决你问题的最简单的方法吗? 不是 OP,但我认为这个问题仍然有效:当您拥有相当原子的业务逻辑时,CallBase 是最简单的解决方案,通过单个入口点进行测试非常麻烦。拆分为类是一个很好的原则,但有时对于遗留代码来说代价高昂,或者至少是一个危险的起点。过度分裂也会导致类型腹泻,这使得解决方案更难阅读(例如,由 10 个相互依赖的类组成的网格,其中只有一个合理的入口点)。 @JouniHeikniemi 将方法拆分为一个单独的类是 Resharper 会为您做的事情,所以我看不出它有多不安全。如果它编译,你已经完成了这项工作。如果您还根据它的作用命名该类,则该解决方案将更容易阅读而不是更难。结果不应该有相互依赖的类的网格;只是一个辅助类,过去是一个长方法。也适用于大块的谨慎功能。请注意,我重构遗留代码并对其进行测试为了好玩... 当然,太好了!但请注意,我的要点是一个极其简化的示例,只是为了可视化重构工具的局限性。我碰巧面临一个 2500+ 行的文件——一个类——至少有 10 种我可以考虑单独测试的方法。但是在大约 15 个共享数据成员(一些位于基类中,它也有自己的一组方法)和大量交叉调用(其中一些基于委托)之间,我发现在不引入显着性的情况下拆分类非常困难改变风险。为此,CallBase 实际上感觉是一种很好的 first 方法。【参考方案2】:

我相信 Lunivore 的答案在撰写时是正确的。

在较新版本的 Moq 中(我认为从 2013 年的 4.1 版开始),可以使用您建议的确切语法来做您想做的事情。那就是:

mock.Setup(m => m.VirtualMethod()).CallBase();

这设置了松散的模拟来调用VirtualMethod 的基本实现,而不是仅仅返回default(WhatEver),但只针对这个成员(VirtualMethod)。


正如用户 BornToCode 在 cmets 中指出的那样,如果方法具有 返回类型 void,这将不起作用。当VirtualMethod 为非void 时,Setup 调用会给出一个Moq.Language.Flow.ISetup&lt;TMock, TResult&gt;,它从Moq.Language.Flow.IReturns&lt;TMock, TResult&gt; 继承CallBase() 方法。但是当方法为 void 时,我们会得到一个 Moq.Language.Flow.ISetup&lt;TMock&gt;,而它缺少所需的 CallBase() 方法。

更新: andrew.rockwell 在下面指出,它现在适用于 void 方法,显然这已在 4.10 版(从 2018 年开始)中得到修复。

【讨论】:

我不再在日常工作中使用 C#,因此无法进行验证,但我很乐意更新已接受的答案以反映当前现实。如果其他人可以加入以确认@Jeppe 的新答案,请这样做! ——谢谢 @Daryn:我可以确认它有效,只是尝试过。阅读这个问题最终意外地帮助我理解了一个不相关的问题(为什么我所有的非虚拟方法都调用它们的基本实现?),所以我想我会回报你的帮助并试一试:) 如果 VirtualMethod() 返回 void 则不起作用。有没有办法让它也适用于 void 方法? @BornToCode 显然不是。我在回答中提到了这个缺点。感觉就像 Moq 中的一个错误(或缺失的功能)。 从今天开始为我使用 void 方法【参考方案3】:

一种方法可以调用真正的方法并且在方法为void 时有一个回调,但它真的 hacky .你必须让你的回调明确地调用它,并欺骗 Moq 调用真正的。

例如,给定这个类

public class MyInt

    public bool CalledBlah  get; private set; 

    public virtual void Blah()
    
        this.CalledBlah = true;
    

您可以这样编写测试:

[Test]
public void Test_MyRealBlah()

    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    bool calledBlah = false;
    m.When(() => !calledBlah)
        .Setup(i => i.Blah())
        .Callback(() =>  calledBlah = true; m.Object.Blah(); )
        .Verifiable();

    m.Object.Blah();

    Assert.IsTrue(m.Object.CalledBlah);
    m.VerifyAll();

关键方面是您跟踪是否已调用假版本,然后将模拟设置为如果已调用假版本。

如果你接受 args 并且值很重要,你仍然可以做类似的事情:

public class MyInt

    public List<int> CalledBlahArgs  get; private set; 

    public MyInt()
    
        this.CalledBlahArgs = new List<int>();
    

    public virtual void Blah(int a)
    
        this.CalledBlahArgs.Add(a);
    


[Test]
public void Test_UpdateQueuedGroups_testmockcallback()

    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    List<int> fakeBlahArgs = new List<int>();

    m.Setup(i => i.Blah(It.Is<int>(a => !fakeBlahArgs.Contains(a))))
        .Callback<int>((a) =>  fakeBlahArgs.Add(a); m.Object.Blah(a); )
        .Verifiable();

    m.Object.Blah(1);

    Assert.AreEqual(1, m.Object.CalledBlahArgs.Single());
    m.Verify(b => b.Blah(It.Is<int>(id => 1 == id)));

【讨论】:

有趣。但是,您设置了 m.CallBase = true;,但提问者明确表示他想要一个解决方案,您不将 CallBase 设置为 True,因为这也会影响所有其他 virtual 成员,而提问者希望避免这种情况。跨度>

以上是关于使用 Moq 模拟类时,如何仅针对特定方法进行 CallBase?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 MOQ 框架在 c# 中模拟静态方法?

初学 Moq

如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory

Moq,SetupGet,模拟属性

Moq:设置一个模拟方法在第一次调用时失败,在第二次调用时成功

如何使用 Moq 框架模拟 ModelState.IsValid?