【问题标题】:How to manage entity dependencies when testing a use case in Clean Architecture ( or DDD )在 Clean Architecture(或 DDD)中测试用例时如何管理实体依赖项
【发布时间】:2021-12-05 21:10:17
【问题描述】:

备注:

  • 我使用术语 mocking 作为通用术语,意思是所有类型的测试替代品,包括间谍、伪造品、模拟、存根和所有其他东西。
  • 在写我的问题时,我只谈到了干净架构中的“用例”,但我的问题还涉及 DDD 中的“域服务”。

我们走吧:

我目前正在尝试在一个新项目中实施 DDD 和清洁架构原则。但是,我已经坚持了 1 周 ---> 如何为我的用例编写单元测试

这是我的问题:

在 Clean Architecture 中,当我们创建一个用例(或 DDD 中的域服务)时,它在大多数情况下将取决于一定数量的实体 + 其余部分(存储库、api ...)

要编写我的用例的单元测试,我从以下内容开始:

- 模拟与外部交互的“其余”依赖项(存储库、API ...)

- 但是接下来,我应该如何处理单元测试中的实体依赖项?

以下是我想到的解决方案:

  • 解决方案 1: 我在注入虚假实体

- 但是,通过阅读有关单元测试最佳实践的内容,我了解到我们应该尽可能避免创建模拟,因为它们是“代码味道”,而一个好的设计应该可以不用它们。

- 事实上,嘲笑我的实体意味着我削弱了我的测试。测试将与我的模拟实体紧密耦合。

- 此外,重新创建实体的结构对我来说似乎毫无意义......

* 如果我的用例使用多个实体方法:那么我必须重新创建每个方法的返回值。

* 如果我的实体的结构很复杂,我最终会写出复杂的假货,因此我的测试会失去很多可靠性,而且我的假货更有可能是错误的,而不是我原来的实体)

* 更糟糕的是,如果我使用工厂,那么我将不得不制造工厂的假货 -> 假货将不得不建立一个假实体......

  • 解决方案 2: 我不模拟实体。

- 另一方面,如果我不模拟我的实体,那么我认为我会采用集成测试的方式:测试不同实体之间的交互...

- 也正如一些模拟支持者所指定的:如果我不模拟我的依赖项,那么即使我的测试单元是有效的,如果我的依赖项导致错误,测试也会失败。这将在我的测试中导致错误的警报信号...

  • 解决方案 3:重构生产代码

- 通过阅读几篇文章,一些人提供了限制耦合的解决方案(将副作用与其余逻辑隔离,将逻辑与 I/O 隔离,使用纯函数,依赖注入,...)但即使通过应用所有这一切,一个用例将不可避免地需要这些实体来工作......因此它不是一个解决方案。

但是那怎么办呢?我错过了什么吗?
如何对用例(或 DDD 中的服务域)进行 GOOD 单元测试? : 在单元测试中有实体依赖的单元测试中如何管理实体?

示例:

为了说明我的问题并更好地理解您的答案,这里是一个虚构的问题示例:

假设我有以下实体:

+class HorseEntity() 
    +constructor(name,age,health, ...) 
    +equipSaddle() 
    +makeShoeing() 
    +checkHealth() 

我想创建一个用例来为我的马厩添加一匹马:

+class addHorseUseCase() 
    +execute(requestDto,HorseRepo,HorseEntity, ...) 

这个用例是:

  1. 创建马实体
  2. 检查马的健康状况
  3. 给马穿鞋
  4. 为马配备鞍
  5. 并将其添加到马厩中。

当我创建测试时,我应该如何处理“HorseEntity”依赖项?

我的问题总结:

- 如何为一个用例编写一个好的单元测试以及如何在我的测试中处理实体依赖关系?
- 一般来说,我应该如何处理单元测试中的实体依赖项?
- 在上面这个例子中,如何为“addHorseUseCase”编写一个好的单元测试?

感谢您以后的回答。

PS:我将这个问题从法语翻译成英语,如果您不理解我的其中一个句子的措辞,请随时告诉我,以便我进行编辑。

【问题讨论】:

    标签: unit-testing architecture domain-driven-design clean-architecture


    【解决方案1】:

    您正在寻找“什么是有意义的?”回答,所以我只能从我的角度告诉你什么是有意义的。

    首先你不需要mock所有依赖或者你在其他测试中mock字符串和数组列表对象?

    我的目标是让我的单元测试尽可能快且易于设置。

    1. 如果它们在纯 POJO 上运行,测试将会非常快。您的用例使用实体并且实体是 POJO,因此您的用例测试将运行得很快。
    2. 我模拟提供实体的存储库,因为存储库通常将用例连接到外部系统,例如数据库或休息服务。这些系统会使测试变得缓慢且难以设置。

    所以回答你的问题...

    如何为一个用例编写一个好的单元测试以及如何在我的测试中处理实体依赖关系?

    使用解决方案 2:我不模拟实体。我通常会这样做。

    一般来说,我应该如何处理单元测试中的实体依赖项?

    您可以模拟它们,但这会使您的代码更加复杂。所以只需使用它们并确保它们是普通对象。

    在上面这个例子中,如何为“addHorseUseCase”编写一个好的单元测试?

    +class addHorseUseCase() 
        +execute(requestDto,HorseRepo,HorseEntity, ...) 
    
    1. 创建一个HorseRepo 模拟一个requestDto、一个HorstEntity 以及您需要调用用例的任何内容。
    2. 调用用例。
    3. 对用例响应做出断言。

    编辑

    我还决定不模拟我的实体,以便进行最简单的单元测试。那么,您不觉得自己在进行集成测试吗?

    这是一种集成测试,因为您测试组合在一起的各个软件模块。但是您不与外部系统集成。我想这是关于定义集成对您意味着什么。

    所以这是我的 3 个定义测试:

    • unit:一个单独的类或函数的api。

    • 组件:在不涉及外部系统的情况下为一个问题服务的一组类或函数。

    • 集成:与外部系统的交互行为 - 它们的 api。

    我对这些不同测试的权重最好用金字塔可视化。

                  /\       <-- integration
                 /--\
                /    \     <-- component
               /      \
              /--------\   
             /          \  <-- unit
            +------------+
    

    如您所见,我通常会尽可能多地进行组件和单元测试,并且根据需要(必需)进行很少的集成测试。我这样做是为了实现我的目标:“我想要尽可能多的测试,这些测试运行速度快且易于设置。”

    现在可能会出现一个问题:“故障定位呢?”

    例如当您对用例和实体层进行隔离测试时,很容易看出哪里发生了错误。当测试同时测试两者时,您不能说错误是发生在实体层还是用例层。没错,但您还应该对实体进行单元测试。如果是这样,它们也可能会失败,并且作为架构后果(用例取决于实体),您要么在用例和实体层失败,要么用例失败是实体层失败的结果。因此,您应该遵循架构并首先修复实体测试,因为用例依赖于它们。之后,您将查看是否有任何用例失败。

    我想说的是,通过模拟实体来隔离用例测试在故障定位方面比组件测试有一点优势,但会创建更多代码来管理。所以这是一个权衡。

    【讨论】:

    • 感谢您的回复。之后继续想。为了进行最简单的单元测试,我还决定不模拟我的实体。那么,您不觉得自己在进行集成测试吗?
    • @ThezozolinoL 我尽量回答你的其他问题,但是关于测试还有很多要讨论的。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多