【问题标题】:Using Moq to override virtual methods in the same class使用 Moq 覆盖同一类中的虚拟方法
【发布时间】:2010-08-21 22:25:30
【问题描述】:

我们正在使用 Moq 对我们的服务类进行单元测试,但是却被困在如何测试服务方法调用同一类的另一个服务方法的情况上。我尝试将被调用的方法设置为虚拟,但仍然无法弄清楚在起订量中该怎么做。例如:

public class RenewalService : IRenewalService
{
    //we've already tested this
    public virtual DateTime? GetNextRenewalDate(Guid clientId)
    {
        DateTime? nextRenewalDate = null;
        //...<snip> a ton of already tested stuff...

        return nextRenewalDate;
    }

    //but want to test this without needing to mock all 
    //the methods called in the GetNextRenewalDate method
    public bool IsLastRenewalOfYear(Renewal renewal)
    {
        DateTime? nextRenewalDate = GetNextRenewalDate(renewal.Client.Id);
        if (nextRenewalDate == null)
            throw new Exceptions.DataIntegrityException("No scheduled renewal date, cannot determine if last renewal of year");
        if (nextRenewalDate.Value.Year != renewal.RenewDate.Year)
            return true;
        return false;
    }
}

在上面的示例中,我们的 GetNextRenewalDate 方法非常复杂,我们已经对其进行了单元测试。但是,我们想要测试更简单的 IsLastRenewalOfYear,而不需要模拟 GetNextRenewalDate 所需的所有内容。基本上,我们只想模拟 GetNextRenewalDate。

我意识到我可以创建一个覆盖 GetNextRenewalDate 的新类并测试新类,但有没有一种方法可以利用 Moq 来简化这件事?

【问题讨论】:

    标签: c# unit-testing moq


    【解决方案1】:

    您可能可以在这种情况下使用部分模拟,尽管您的所有方法都需要是虚拟的:

        var mock = new Moq.Mock<RenewalService>();
        mock.Setup(m => m.GetNextRenewalDate(It.IsAny<Guid>())).Returns(null);
        mock.CallBase = true;
        var results = mock.Object.IsLastRenewalOfYear(...);
    

    【讨论】:

    • 这似乎可行...我什至不需要将所有内容都虚拟化(我只是保持与上面相同)。
    • @Andrew。上述工作是因为Mock.CallBase == true 意味着与设置不匹配的调用将调用底层实现。因此,IsLastRenewalOfYear 将调用实现,因为它不是虚拟的,但GetNextRenewalDate 将返回null,因为设置将始终匹配。即使IsLastRenewalOfYear 是虚拟的,以下代码也将起作用。
    • 我们也在其他几个地方使用过它,效果很好。谢谢!
    • mock.Object 需要一个构造器的情况呢?执行方法时报错:Can not instantiate proxy of class: (classNameGoesHere)。找不到无参数构造函数。参数名称:constructorArguments
    • @MattyBear Mock("Blah Blah")
    【解决方案2】:
    var mock = new Moq.Mock<RenewalService> { CallBase = true };
    mock.Setup(m => m.GetNextRenewalDate(It.IsAny<Guid>())).Returns(null);
    var results = mock.Object.IsLastRenewalOfYear(...);
    

    【讨论】:

      【解决方案3】:

      编辑:我现在同意这不是 Andrew 案例的正确答案。我想把这个答案留给 cmets 线程。请不要再投反对票了:)

      编辑前:

      通常模拟对象框架并非旨在简化单类方案,它们旨在隔离您的代码,以便您可以测试单个类。

      如果您尝试使用模拟对象框架来解决这个问题,那么框架只会创建一个派生类并重载该方法。唯一不同的是您可以在大约 3 行而不是 5 行中完成此操作,因为您不必创建派生类定义。

      如果您想使用模拟对象来隔离这种行为,那么您应该稍微拆分一下这个类。 GetNextRenewalDate 逻辑可以存在于 RenewalService 对象之外。

      您遇到此问题的事实可能表明还有更简单或更细粒度的设计尚待发现。找到一个不太具体的类,名称如“manager”或“service”通常暗示您可以将设计分解为更小的类,并从中获得更好的可重用性和可维护性。

      【讨论】:

      • 我不确定拆分我们的设计是否会使其更易于维护......如果我们从 5,000 个类(大约是我们目前拥有的)增加到 10,000 个类,导航将几乎不会更容易.
      • 仅仅为了使其在特定工具集中更具可测试性而分解设计似乎是个坏主意。沿着这条路走,你很少有方法调用类中的其他公共方法。这么好的设计怎么样?
      • @Andrew, Jess:我不知道他的应用程序或他的要求,所以请随时接受或离开我的建议。是的,仅仅为了有一个方法类而分解它是愚蠢的,但我的其余建议仍然有效。很多时候,当我看到“管理器”或“服务”类时,抽象不在正确的位置。如果要进行这样的重构,其他类的方法可能会在这里结束,或者这些方法可能会在其他类中结束,从而使这个类完全消失。
      • @Andrew, Jess:至于回答这个问题,我想我应该对此发表评论 :) 我想我认为create a new class that overrides GetNextRenewalDate,他的意思是为这门课放弃起订量,并写作模拟/测试加倍自己。我现在明白了,这可能不是他的意思。
      • 我取消了我的反对票 - 你的编辑很有意义。我同意有时需要重构,但如果类内依赖项是标准之一,那么我们都会有疯狂(且难以管理)的类扩散。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-12
      • 2013-06-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-19
      相关资源
      最近更新 更多