使用Moq模拟类时,如何使用CallBase来获取特定方法?

时间:2010-05-12 21:39:54

标签: unit-testing moq

我非常感谢Moq的Loose模拟行为,在没有设定期望时返回默认值。方便并节省代码,它也可以作为一种安全措施:在单元测试期间不会无意中调用依赖项(只要它们是虚拟的)。

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

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

  1. 为隐藏依赖项的所有属性和方法设置存根 - 即使我的测试认为它不需要关心这些依赖项,或
  2. 希望我不要忘记设置任何存根(并且以后没有新的依赖项添加到代码中) - 风险单元测试会产生真正的依赖关系。
  3. 我认为我想要的是: mock.Setup(m => m.VirtualMethod()).CallBase();
    所以当我打电话给mock.Object.VirtualMethod()时,Moq会调用真正的实现......

    问:使用Moq,有什么方法可以测试虚拟方法,当我模拟类只存在几个依赖项时?即没有诉诸 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";
    }
    

3 个答案:

答案 0 :(得分:10)

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

在较新版本的Moq(我认为从2013年开始的4.1版本)中,您可以使用您提出的确切语法来执行您想要的操作。那就是:

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

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

正如用户BornToCode在评论中注明的那样,如果方法返回类型void ,则无效。当VirtualMethod无效时,Setup调用会生成一个Moq.Language.Flow.ISetup<TMock, TResult>,其继承CallBase()中的Moq.Language.Flow.IReturns<TMock, TResult>方法。但是当该方法无效时,我们会得到一个Moq.Language.Flow.ISetup<TMock>而不是所需的CallBase()方法。

答案 1 :(得分:8)

由于没有人多久回答这个问题,我认为值得回答,我将集中讨论您提出的最高级问题:如果测试中的方法是虚拟的,如何保持这些好处。

快速回答:你不能用Moq做,或者至少不能直接用。但是,你可以做到。

假设你有两个方面的行为,其中方面A是虚拟的而方面B则不是。这几乎反映了你在课堂上的成就。 B可以使用其他方法吗?这取决于你。

目前,你的班级Foo正在做两件事 - A和B.我可以说它们是独立的责任,因为你想要模拟A并自己测试B. p>

您可以:

,而不是试图模拟虚拟方法而不嘲笑其他任何东西
  • 将行为A移至单独的班级
  • 依赖项将带有A的新类注入FooFoo的构造函数
  • 从B。
  • 调用该课程

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

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

希望这有帮助!

答案 2 :(得分:-1)

一种方法来调用真正的方法在方法为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)));
}