【问题标题】:How to mock instance methods called by the tested method so you test only one thing?如何模拟被测试方法调用的实例方法,以便只测试一件事?
【发布时间】:2013-10-23 10:23:41
【问题描述】:

假设我有以下课程:

class ToTest : IToTest
{
    public string MethodA()
    {
        return "Test";
    }

    public int MethodB()
    {
        returns a random number;
    }

    public string MethodC()
    {
        return this.MethodA + " " + this.MethodB.ToString();
    }
}

我现在正在测试 MethodC,所以我知道我应该模拟当前实例的 MethodA 和 MethodB,所以我只测试 MethodC 的有效性对吗?

我正在使用 Rhino,我做了以下操作:

ToTest testedObject = new ToTest();

testedObject.Expect(t => t.MethodA).Returns("AString");
testedObject.Expect(t => t.MethodB).Returns(1324");

Assert.AreEqual("AString 1324", testedObject.MethodC());

但我得到一个正确的错误提示,testedObject 不是 Mock。

方法对吗?我应该如何继续?

【问题讨论】:

    标签: c# unit-testing mocking rhino-mocks


    【解决方案1】:

    不,请不要这样写测试用例。单元测试不是“只测试一件事”,它们都是为了确保每个测试都是一个单元,即它不会影响任何其他测试。

    您的所有测试都应该对您的类的公共 API 感兴趣。不要测试内部或私有方法,它们是类内部工作的一部分,不要试图模拟类的一部分来测试其他部分。您对MethodC 的测试必须间接测试MethodAMethodB,否则您的测试毫无意义。

    对于任何有兴趣就如何编写好的单元测试进行精彩演讲的人,我建议您花一个小时观看 NDC 2013 上的 'Ian Cooper: TDD, where did it all go wrong' video

    【讨论】:

    • 不确定我是否明白你的意思。我已经进行了两个测试 MethodA 和 MethodB 的测试,我为什么要在 MethodC 中再试一次呢?如果我在 MethodA 上有错误,我希望 MethodA 测试失败,而不是 MethodC,这就是我专注于测试 MethodC 功能的原因。这 3 个都是公共方法。
    • 那么用一个单元测试测试多个方法是否正确?
    • MethodC 调用 MethodA,因此要测试 MethodC,您必须调用 MethodA。如果你不这样做,测试是没有意义的。您正在测试 MethodC。它调用其他两种方法的事实是您的测试不应该关心的实现细节。
    • 但是如果 MethodA 错误,MethodA 测试和 MethodC 测试会失败。这样可以吗?
    • @SoMoS,绝对是。 MethodC 测试失败,因为 MethodC 的内部工作存在问题。 MethodA 测试失败的事实也会引导您找到问题所在。
    【解决方案2】:

    根据您让我们想象的示例,您的问题的简单答案是您的方法不正确。通过遇到您遇到的麻烦,代码试图告诉您,您在代码中存在一些复杂的问题,这些问题耦合得太紧密了,并且您拥有的不是具有凝聚力的代码,而是具有粘性的代码。 (Glenn Vanderburg 在http://www.vanderburg.org/Blog/Software/Development/cohesion.rdoc 上有一篇关于凝聚力主题的精彩博客文章)

    如果您觉得需要模拟 MethodAMethodB 来测试 MethodC,这表明代码中缺少一些东西之一,甚至可能全部丢失。

    困难告诉你的第一件事是MethodC 可能在代码中的位置错误。 MethodC 很可能属于另一个对象,该对象通过构造函数注入或参数注入将具有 MethodAMethodB 的对象作为依赖项。

    突出的第二件事是您在测试MethodC 时遇到了问题,因为MethodB 依赖于导致我认为是全局/单例行为的原因。您已将MethodB 定义为returns a random number,而不是依赖于填充NumberGenerator 角色的对象。通过显式调用此角色(接口)的依赖关系,它将允许您根据使用情况传递角色的不同实现。根据是生产代码还是测试代码,可以使用和轻松更换的三种明显实现是:

    • RandomNumberGenerator:每次调用时返回一个随机数,
    • a SequentialNumberGenerator:返回序列中的下一个数字(其中可以生成多种类型的序列),例如每次增加一个设定的数字,或者某种数学序列,例如斐波那契或科拉茨
    • ContsistantNumberGenerator:每次调用都返回相同的数字,这在测试中非常很有用。

    您的示例可能告诉您的第三件事是对上一期的改进,指出您不仅依赖于您无法控制的可变状态,而且依赖于非确定性可变状态,您将无法强制进入给定状态,即使通过向对象发送许多其他消息以尝试进入该状态也是如此。我知道您无法发送任何消息来设置环境以在下次调用时返回所需的随机数;即使有这样的方法,所需的设置量也会掩盖您实际尝试测试的内容。

    我希望这些建议之一可以帮助您解决问题。

    【讨论】:

      【解决方案3】:

      这行不通有两个原因(假设您使用的是 Rhino.Mocks):

      • 您不能模拟非虚拟方法。
      • 您只能测试整个实例(或静态成员)。这意味着,当您想在没有其他方法的情况下单独测试类中的方法时,您需要一些解决方法,即通过提供工厂或将 MethodC 移动到与 MethodA 和 MethodB 不同的类中。

      【讨论】:

      • 我同意,但对我来说似乎很奇怪,因为在每个测试中只测试一件事听起来像是测试人员的口头禅,这看起来是一个很常见的问题。
      • 顺便说一句,如果这有帮助,我有一个 IToTest 接口。添加到问题中。
      【解决方案4】:

      您的示例的工作代码将是这样的:

      public class ToTest
      {
          private Random random = new Random();
      
          public virtual string MethodA()
          {
              return "Test";
          }
      
          public virtual int MethodB()
          {
              return random.Next();
          }
      
          public virtual string MethodC()
          {
              return MethodA() + " " + MethodB();
          }
      }
      
      [TestFixture]
      public class tests
      {
          [Test]
          public void Test_MethodC()
          {
              var mocks = new MockRepository();
              ToTest testedObject = mocks.CreateMock<ToTest>();
      
              testedObject.Expect(t => t.MethodA()).Return("AString");
              testedObject.Expect(t => t.MethodB()).Return(1324);
      
              Assert.AreEqual("AString 1324", testedObject.MethodC());
          }
      }
      

      但是!

      这不会通过测试。原因是 Rhino.Mocks 与大多数其他模拟框架一样,不包括 MS Moles/Shims、TypeMock 和其他使用分析器 API 的框架,无法强制类型以从内部查看模拟方法。这是因为模拟框架围绕原始类型创建了一个代理类型,并将模拟逻辑注入到代理中,而不是原始类型本身。

      因此,按照建议,您应该将 MethodAMethodB 提取到单独的依赖项中,以防您在 MethodC 中有需要测试的内容。

      另一种完全可行的方法是从 ToTest 类派生,覆盖 MethodAMethodB,并测试派生的实例类。

      【讨论】:

      • 您只是在模拟中复制 MethodA 和 MethodB 的功能。在更复杂的场景中,您可能会错误地复制该函数,从而可能导致 MethodC 通过测试,但在生产中失败,这会使测试变得多余,并错误地保证一切正常。或者,如果 MethodA 或 MethodB 的行为发生变化,即使被测代码正常运行,也可能导致脆弱的测试失败。这种嘲笑是不好的做法。
      • @DavidArno 通常,我想以这种方式测试 MethodC 以确保它正确处理无效输入。例如,当 MethodC 是具体的,但 AB 是虚拟的时,我想确保在 null 上抛出异常A 收到。这可能是对这种情况的唯一考验。而且,需要记住的是,这是一种无需大量重构/购买昂贵许可证即可使用的方法,同时使用遗留代码。
      • +1 因为最后一句话。使用从原始方法派生的测试类,MethodA 和 MethodB 的 Mocking 工作正常。不,你不会复制这些的实现
      • @galenus,如果 MethodA 和 MethodB 是虚拟的,那么我希望使用私有方法,并且我会让 MethodC 也使用这些私有方法。在测试其他部分时仍然没有任何借口嘲笑课程的一部分。
      • @DavidArno 我相信有一种纯粹的方式,也有一种方便的方式。如果你想(并且有幸)按照书本做所有事情,你选择纯粹的。如果你知道你做了什么并且得到了一些限制的结果,你选择最方便的方法。有时(不是在这种情况下),模拟类的一部分很方便。例如,当被模拟的方法是工厂方法时,不仅由外部消费者使用,而且由声明该方法的类型使用。当然,随之而来的是正确设计的问题,但有时它是既定的现实。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-16
      • 2016-07-20
      • 2020-05-01
      相关资源
      最近更新 更多