这是来自Mockito单元测试框架的引用:
不要模拟值对象
为什么有人甚至想这样做?
因为实例化对象太痛苦了!? => 不是有效的 原因。如果创建新灯具太难,这是一个标志 代码可能需要一些认真的重构。另一种方法是创建 值对象的构建器 - 有一些工具可以做到这一点,包括 IDE插件,龙目岛等。一个人也可以创造有意义的 测试类路径中的工厂方法。
这里的另一句话:
为简单值对象编写模拟没有多大意义 (无论如何应该是不可变的),只需创建一个实例并使用 它。 不值得创建一个接口/实现对来控制返回哪些时间值,只需创建具有适当值的实例并使用它们即可。当一个类不值得嘲笑时,有几个启发式方法。首先,它只有访问器或简单的方法来作用于它所持有的值,它没有任何有趣的行为。其次,除了VideoImpl或一些模糊的术语之外,你无法为类想出一个有意义的名称。
这似乎是哑值对象仅保存值而仅保留其他值的正当理由,但是当您有一个引用实体和其他值对象的 ValueObject 时,事情会变得更加复杂。
假设我有个人和宠物对象,它们是实体和关系(所有者,医生等),它是两个人之间的价值对象,并且具有关系类型,也是价值对象。所以,关系基本上是:
class Relationship {
private Person person;
private Pet pet;
private RelationshipType type;
}
现在,假设我有一个带有谓词的类,例如isOwnerRelations,isDoctorRelations,等等。基本上谓词很简单
relationship -> relations.isOwner();//delegateto relationsType.isOwner()
现在,我想测试谓词,我有两个选择:
模拟关系
public void testIsOwner() {
Relationship rel = mock(Relationship.class);
when(rel.isOwner()).thenReturn(true);
assertTrue(RelationshipPredicates.isOwner(rel));
}
不要嘲笑关系
public void testIsOwner() {
Person person = PersonBuilder.newPerson();
Pet pet = PetBuilder.newDogPet();
RelationshipType type = RelationshipTypes.ownerType();
Relationship rel = new Relationship(person, pet, type);
assertTrue(RelationshipPredicates.isOwner(rel));
}
当然,这个例子过于简化,因为对于一个人,您可能需要提供地址,对于宠物,您可能需要提供 BreedType,无论如何,即您可能需要提供的实体和价值对象的传递图可能非常巨大。当然,你可以模拟实体,但假设你在关系中有更多的价值对象。即使你有花哨的构建器,你也必须提供原始 ValueObject 的每个部分,即使单元测试只测试它的单个方面。
在谓词测试中,如果谓词关心调用对象的一个特定方法或它们的组合,我为什么要关心完整的对象构造?
还是这样的值对象不能被视为简单且规则不适用?
单元测试应该测试单个单元。因此,如果您的ValueObject
足够简单,那么它不应该影响SUT(被测试对象)的测试。但是,如果ValueObject
具有复杂的行为,那么您应该模拟它。这简化了测试,并将测试隔离到仅 SUT。
我知道这个答案来得有点晚,但在我看来,你应该尽量让事情变得更简单。当对象创建无法给测试带来副作用时,使用"真实的东西"(使用构造函数),例如,当您只需要方法的某个返回值时,请使用模拟/存根。它是否是值对象并不重要。
例如,值对象可以使用随机数生成器在构造时为其属性之一提供值。这可能会对您的测试产生副作用(因为熵,在某些情况下不足以生成该数字),因此最好改用存根/模拟。
或者,如果你是一个完美主义者,想要过度设计你的解决方案,你可以有一个纯值对象,并将构造移动到工厂类,将随机数生成器移动到接口/基础结构类(域层中的 IRandomNumberGenerator,基础结构层中的 RandomNumberGenerator,这将需要集成测试/压力测试以查看随机源有多好)。在这种情况下,您应该在测试中使用真实的东西,构造真正的值对象,因为副作用已移动到其他类。
应用KISS(保持简单愚蠢)规则。模拟以避免测试中的副作用,并且只编写一行代码(当你只需要从方法返回某个代码时),否则使用真实的东西(这通常比在更复杂的行为中存根这么多方法要简单得多)。
只需使用使您的代码更短、更简单、更易于遵循的任何内容,但始终记住要注意可能带来副作用的对象。
在谓词测试中,如果谓词关心调用对象的一个特定方法或它们的组合,我为什么要关心完整的对象构造?
如果谓词只关心调用一个特定的方法,那么为什么要向它传递整个值呢?
在测试驱动设计中,关键思想之一是编写测试是关于你正在创建的API的反馈;如果你发现该API测试起来很笨拙,这表明API也可能使用起来很笨拙。
在这种特定情况下,测试试图告诉您当前的设计违反了接口隔离原则。
不应强制任何客户端依赖于它不使用的方法。
谓词关心的只是所有权的描述,所以也许这个想法应该在你的解决方案中明确表达
interface DescribesOwnership {
boolean isOwner();
}
class Relationship implements DescribesOwnership {
@Override
boolean isOwner() {
return this.type.isOwner();
}
}
这是一个可能的答案。 另一个是代码试图告诉你,用于构造Relationship
的 API 需要一些工作。 你正在朝着这个方向前进,你的Builder
提案,但同样......听测试。
它试图告诉你你想要:
Relationship rel = Relationship.builder()
.using(RelationshipTypes.ownerType())
.build();
换句话说,这个测试根本不关心主人或宠物使用什么值;它甚至不关心这些东西的存在。 也许在其他地方也是如此。
请注意,您仍然可以获得模拟示例中的干净测试
public void testIsOwner() {
Relationship rel = Relationship.builder()
.using(RelationshipTypes.ownerType())
.build();
assertTrue(RelationshipPredicates.isOwner(rel));
}
不喜欢Builder
成语? 没关系;请注意,我们真正要做的是在RelationshipTypes
实例和Relationship
实例之间创建映射。 换句话说,您只是在寻找一个功能。
public void testIsOwner() {
// foo: RelationshipTypes -> Relationship
Relationship rel = foo(RelationshipTypes.ownerType());
assertTrue(RelationshipPredicates.isOwner(rel));
}
您可以使用与本地编码风格一致的foo
拼写。
总结
不要模拟值对象
这似乎是非常好的建议 - 提醒您为值对象创建模拟正在解决错误问题是一种启发式方法。