不要嘲笑值对象:太通用的规则,没有解释



这是来自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拼写。

总结

不要模拟值对象

这似乎是非常好的建议 - 提醒您为值对象创建模拟正在解决错误问题是一种启发式方法。

相关内容

  • 没有找到相关文章

最新更新