【问题标题】:Should I mock all the direct dependencies of a class in unit tests?我应该在单元测试中模拟一个类的所有直接依赖项吗?
【发布时间】:2016-12-15 05:40:49
【问题描述】:

假设我们有一个类Controller 依赖于一个类Service,而Service 类依赖于一个类Repository。只有 Repository 与外部系统(比如 DB)通信,我知道在执行单元测试时应该模拟它。

我的问题:对于单元测试,我是否应该在测试 Controller 类时模拟 Service 类,即使 Service 类不直接依赖于任何外部系统?为什么?

【问题讨论】:

    标签: unit-testing


    【解决方案1】:

    这取决于您编写的测试类型:集成测试或单元测试。 我假设您想在这种情况下编写单元测试。单元测试的目的是仅测试您的类的业务逻辑,因此应模拟所有其他依赖项。

    在这种情况下,您将模拟 Service 类。这样做还允许您根据传递给Service 的某个方法的输入来准备测试某些场景。想象一下,您的Service 中有一个Person findPerson(Long personID) 方法。在测试您的Controller 时,您对让Service 实际返回正确输出所必需的一切都不感兴趣。对于 Controller 的某个测试场景,您只希望它返回 Person,而对于不同的测试场景,您不希望它返回任何内容。模拟使这很容易。

    另请注意,如果您模拟您的 Service,则不必模拟 Repository,因为您的 Service 已经是模拟的。

    TLDR;在为某个类编写单元测试时,只需模拟所有其他依赖项,以便能够操纵对这些依赖项进行的方法调用的输出。

    【讨论】:

    • 您的假设是正确的:我想编写单元测试。现在我知道了单元测试应该是怎样的。非常感谢!
    【解决方案2】:

    是的,在测试控制器时模拟服务。单元测试有助于识别回归的位置。因此,仅当服务代码已更改而不是控制器代码已更改时,服务代码的测试才会失败。这样,当服务测试失败时,您就可以确定根本原因在于对服务的更改。

    此外,通常模拟服务比模拟服务调用的所有存储库只是为了测试控制器要容易得多。因此,它使您的测试更易于维护。

    但一般来说,您可以保持某些 util 类不被模拟,因为通过模拟它们获得的收益多于损失。另见: https://softwareengineering.stackexchange.com/questions/148049/how-to-deal-with-static-utility-classes-when-designing-for-testability

    【讨论】:

      【解决方案3】:

      与所有工程问题一样,TDD 也不例外。答案总是“视情况而定”。总会有取舍。

      在 TDD 的情况下,您首先通过行为预期来开发测试。根据我的经验,行为预期是一个单位。

      举个例子,假设您想获取以姓氏“A”开头的所有用户,并且他们在系统中处于活动状态。因此,您将编写一个测试来创建一个控制器操作,以获取以“A”public ActionResult GetAllActiveUsersThatStartWithA() 开头的活跃用户。

      最后,我可能会有这样的事情:

      public ActionResultGetAllActiveUsersThatStartWithA()
      {
          var users = _repository.GetAllUsers();
          var activeUsersThatStartWithA = users.Where(u => u.IsActive && u.Name.StartsWith('A');
          return View(activeUsersThatStartWithA);
      }
      

      这对我来说是一个单位。然后我现在可以重构(通过使用以下方法添加 service 类来更改我的实现而不改变行为)

      public IEnumerable<User> GetActiveUsersThatStartWithLetter(char startWith)
      {
          var users = _repository.GetAllUsers();
          var activeUsersThatStartWithA = users.Where(u => u.IsActive && u.Name.StartsWith(startsWith);
      }
      

      我的控制器的新实现变成了

      public ActionResultGetAllActiveUsersThatStartWithA()
      {
          return View(_service.GetActiveUsersThatStartWithLetter('A');
      }
      

      这显然是一个非常人为的例子,但它说明了我的观点。这样做的主要好处是我的测试不依赖于除repository 之外的任何实现细节。然而,如果我在我的测试中模拟了 service,我现在与该实现相关联。如果出于某种原因删除了 service 层,我的所有测试都会中断。我会发现service 层比repository 层更容易更改。

      要考虑的另一件事是,如果我在控制器类中模拟 service,我可能会遇到我的所有测试都正常工作的情况,但我发现系统已损坏的唯一方法是通过集成测试(意味着进程外,或装配组件相互交互),或通过生产问题。

      例如,如果我将 service 类的实现更改为以下:

      public IEnumerable<User> GetActiveUsersThatStartWithLetter(char startsWith)
      {
          throw new Exception();
      }
      

      同样,这是一个非常人为的例子,但这一点仍然是相关的。我的controller 测试无法捕捉到这一点,因此看起来系统在我通过“单元测试”时表现正常,但实际上系统根本无法正常工作。

      我的方法的缺点是测试可能会变得非常繁琐。因此,权衡是在测试复杂性与抽象/可模拟实现之间取得平衡。

      要记住的关键是,TDD 提供了捕获回归的好处,但它的主要好处是帮助设计系统。换句话说,不要让设计决定你编写的测试。让测试先决定系统的功能,然后再通过重构来关注设计。

      【讨论】:

        猜你喜欢
        • 2014-07-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-03-19
        • 2023-03-13
        • 1970-01-01
        • 2013-07-07
        相关资源
        最近更新 更多