DDD和单元测试,我应该直接创建实体还是通过它们的域服务



一些被测试的实体,不能直接使用构造函数创建,而只能通过域服务创建,因为需要使用存储库,可能是为了一些需要在DB中进行的验证(想象一个唯一的代码验证)。

在我的测试中,我有两个选项:

  1. 使用公开实体创建的域服务创建实体,这需要我模拟该服务所需的所有存储库接口,并指示相关的接口正确运行以成功创建
  2. 以某种方式直接使用实体构造函数(我使用c#,所以我可以向测试程序集公开一个内部构造函数)并绕过服务逻辑获得实体。

我不确定哪一个是最好的方法,
我更喜欢第一种方法,因为它测试域模型的公共行为,因为从外部的角度来看,创建实体的唯一方法是通过域服务。但是由于需要模拟配置,这个解决方案带来了大量的"Arrange"代码。第二种方法更直接,它绕过服务逻辑创建对象,但这是对域模型的一种欺骗,它假设测试代码知道域模型的内部结构,这不是一个好的观点。但是代码更容易读。

我在测试中使用构建器创建实体,因此第一种方法所需的配置代码将在构建器代码中隔离,但我仍然想知道正确的方法是什么。

本质上你是在问你应该在什么"水平"上进行测试。选项2是一个单元测试,因为它只测试单个类的代码。选项1更像是一个集成测试,因为它会同时测试几个组件。

对于单元测试,我倾向于选择选项2,原因如下:

  1. 如果单元测试只测试单个类,则更简单、更有效。如果您使用工厂服务来创建要测试的对象,那么您的测试就不能直接控制对象的构造方式。这将导致混乱和乏味的测试代码,比如模拟所有的存储库接口。

  2. 我通常会在我的测试代码库的不同部分进行实际的集成测试(或验收测试),通过它的公共接口(与外部依赖关系,如数据库模拟/存根)从头到尾测试整个应用程序。我希望这些测试涵盖了你问题中的选项1,所以我真的不需要在单元测试套件中重复选项1。

您可能会问,启动整个应用程序只是为了测试几个类有什么意义?答案很简单——只要坚持两层测试,你的测试代码库就会干净、易读并且易于重构。如果你的测试在测试的"级别"上非常多样化(有些测试单个类,有些测试两个类一起,有些测试整个应用程序),那么测试代码就会变得难以维护。

一些事项:

  1. 这个建议适用于你正在开发一个将要被部署和运行的"应用程序"。如果你正在开发一个"共享库",并将其分发给其他团队使用,那么你应该从所有公共入口点进行测试,而不管"级别"如何。(但我仍然不会称这些测试为"单元测试",而是将它们在代码库中分开。)
  2. 如果您没有能力编写完整的集成测试,那么我会使用选项1和2。只是要小心测试代码库变得臃肿。

还有一点——如果它们因为相同的原因而改变,那么一起测试它们。选择选项1后,您不希望的情况是每次更改工厂/存储库代码时都必须更改实体测试。如果每个实体的行为没有改变,则不应该更改测试。

您可以通过首先不通过域服务创建实体来避免这个难题。

如果您觉得需要在创建实体之前验证有关实体的某些内容,您可能会将其视为域不变量并由聚合强制执行。聚合根将公开一个方法来创建实体。

只要负责生成新实体的聚合保证了不变量,就可以针对内存中的具体对象测试所有内容,因为聚合内部应该有所有需要的数据来检查不变量——不需要求助于外部存储库。您可以将创建者聚合设置为内存中的不变断开状态或非不变断开状态,并直接在聚合的CreateMyEntity方法上执行测试。

Udi Dahan的

不要创建聚合根是关于这种方法的一个很好的阅读-基本思想是实体和聚合根不是凭空产生的。

最新更新