DDD 和单元测试,我应该直接创建实体还是通过他们的域服务创建实体?
DDD and Unit Test, should I create entities directly or through their Domain service?
一些正在测试的实体,不能直接使用构造函数创建,只能通过域服务创建,因为需要使用存储库,可能是为了一些需要在数据库中命中的验证(想象一个独特的代码验证)。
在我的测试中,我有两个选择:
- 使用公开实体创建的域服务创建实体,这需要我模拟该服务所需的所有存储库接口并指示相关接口正确运行以成功创建
- 以某种方式直接使用实体构造函数(我使用 c#,因此我可以向测试程序集公开内部构造函数)并绕过服务逻辑获取实体。
我不确定哪种方法最好,
第一个是我更喜欢的,因为它测试域模型的 public 行为,因为从外部角度来看,创建实体的唯一方法是通过 Domanin 服务。但是由于需要模拟配置,此解决方案引入了大量 "Arrange" 代码。
第二个更直接,它绕过服务逻辑创建对象,但这是对域模型的一种欺骗,它假设测试代码知道域模型的内部结构,这不是一个好点。但是代码可读性更好一些。
我在测试中使用 Builders 创建实体,因此第一种方法所需的配置代码将在构建器代码中隔离,但我仍然想知道什么是正确的方法。
本质上你是在问 'level' 你应该测试什么。选项 2 在很大程度上是一个单元测试,因为它只会测试单个 class 的代码。选项 1 更像是一个集成测试,因为它将一起测试多个组件。
我倾向于选择选项 2 进行单元测试,原因如下:
单元测试如果只测试单个 class 会更简单、更有效。如果您使用工厂服务创建被测对象,则您的测试无法直接控制对象的构造方式。会导致测试代码杂乱乏味,比如mock所有的repository接口。
我通常会在测试代码库的不同部分进行实际的集成测试(或验收测试),通过它的 public 接口从前到后测试整个应用程序(具有外部依赖项,例如数据库 mocked/stubbed )。我希望这些测试涵盖您问题中的选项 1,因此我真的不需要在单元测试套件中重复选项 1。
您可能会问,启动我的整个应用程序只是为了测试几个 classes 有什么意义?答案很简单——坚持只进行两个级别的测试,您的测试代码库将是干净的、可读的并且易于重构。如果你的测试在他们测试的 'level' 方面有很大差异(有些测试单个 class,有些一起测试几个 class,有些测试整个应用程序)那么测试代码变得难以维护。
一些注意事项:
- 此建议适用于您正在开发将要部署的 "application" 和 运行。如果您正在开发一个 "shared library" 并将分发给其他团队以供他们认为合适的使用,那么您应该从库的所有 public 入口点进行测试,而不管 'level' . (但我仍然不会调用这些测试 "unit tests" 并会在代码库中将它们分开。)
- 如果您没有能力编写完整的集成测试,那么我会使用选项 1 和 2。只是要小心测试代码库变得臃肿。
还有一点 - 如果它们因相同原因而改变,请一起测试。在选择选项 1 后,您 不想 最终遇到的情况是每次更改 factory/repository 代码时都必须更改您的实体测试。如果每个实体的行为都没有改变,那么你不应该改变测试。
您可以通过不首先通过域服务创建您的实体来避免这个难题。
如果您觉得需要在创建实体之前验证有关实体的某些内容,您可能会将其视为域不变量并由聚合强制执行。该聚合根将公开一个创建实体的方法。
只要负责生成新实体的聚合保证不变量,就可以针对内存中的具体对象测试所有内容,因为聚合本身应该包含所有需要的数据来检查不变量——没有求助于外部存储库。您可以将创建者聚合设置为在内存中处于不变中断状态或非不变中断状态,并直接在聚合的 CreateMyEntity
方法上进行测试。
Don't Create Aggregate Roots 作者 Udi Dahan 是该方法的好读物 - 基本思想是实体和聚合根并非凭空诞生。
一些正在测试的实体,不能直接使用构造函数创建,只能通过域服务创建,因为需要使用存储库,可能是为了一些需要在数据库中命中的验证(想象一个独特的代码验证)。
在我的测试中,我有两个选择:
- 使用公开实体创建的域服务创建实体,这需要我模拟该服务所需的所有存储库接口并指示相关接口正确运行以成功创建
- 以某种方式直接使用实体构造函数(我使用 c#,因此我可以向测试程序集公开内部构造函数)并绕过服务逻辑获取实体。
我不确定哪种方法最好,
第一个是我更喜欢的,因为它测试域模型的 public 行为,因为从外部角度来看,创建实体的唯一方法是通过 Domanin 服务。但是由于需要模拟配置,此解决方案引入了大量 "Arrange" 代码。
第二个更直接,它绕过服务逻辑创建对象,但这是对域模型的一种欺骗,它假设测试代码知道域模型的内部结构,这不是一个好点。但是代码可读性更好一些。
我在测试中使用 Builders 创建实体,因此第一种方法所需的配置代码将在构建器代码中隔离,但我仍然想知道什么是正确的方法。
本质上你是在问 'level' 你应该测试什么。选项 2 在很大程度上是一个单元测试,因为它只会测试单个 class 的代码。选项 1 更像是一个集成测试,因为它将一起测试多个组件。
我倾向于选择选项 2 进行单元测试,原因如下:
单元测试如果只测试单个 class 会更简单、更有效。如果您使用工厂服务创建被测对象,则您的测试无法直接控制对象的构造方式。会导致测试代码杂乱乏味,比如mock所有的repository接口。
我通常会在测试代码库的不同部分进行实际的集成测试(或验收测试),通过它的 public 接口从前到后测试整个应用程序(具有外部依赖项,例如数据库 mocked/stubbed )。我希望这些测试涵盖您问题中的选项 1,因此我真的不需要在单元测试套件中重复选项 1。
您可能会问,启动我的整个应用程序只是为了测试几个 classes 有什么意义?答案很简单——坚持只进行两个级别的测试,您的测试代码库将是干净的、可读的并且易于重构。如果你的测试在他们测试的 'level' 方面有很大差异(有些测试单个 class,有些一起测试几个 class,有些测试整个应用程序)那么测试代码变得难以维护。
一些注意事项:
- 此建议适用于您正在开发将要部署的 "application" 和 运行。如果您正在开发一个 "shared library" 并将分发给其他团队以供他们认为合适的使用,那么您应该从库的所有 public 入口点进行测试,而不管 'level' . (但我仍然不会调用这些测试 "unit tests" 并会在代码库中将它们分开。)
- 如果您没有能力编写完整的集成测试,那么我会使用选项 1 和 2。只是要小心测试代码库变得臃肿。
还有一点 - 如果它们因相同原因而改变,请一起测试。在选择选项 1 后,您 不想 最终遇到的情况是每次更改 factory/repository 代码时都必须更改您的实体测试。如果每个实体的行为都没有改变,那么你不应该改变测试。
您可以通过不首先通过域服务创建您的实体来避免这个难题。
如果您觉得需要在创建实体之前验证有关实体的某些内容,您可能会将其视为域不变量并由聚合强制执行。该聚合根将公开一个创建实体的方法。
只要负责生成新实体的聚合保证不变量,就可以针对内存中的具体对象测试所有内容,因为聚合本身应该包含所有需要的数据来检查不变量——没有求助于外部存储库。您可以将创建者聚合设置为在内存中处于不变中断状态或非不变中断状态,并直接在聚合的 CreateMyEntity
方法上进行测试。
Don't Create Aggregate Roots 作者 Udi Dahan 是该方法的好读物 - 基本思想是实体和聚合根并非凭空诞生。