以下是Mockito单元测试框架的引用:
不要模拟价值对象
为什么人们甚至想要这样做?
因为实例化对象太痛苦了! =>不是有效的 原因。如果创建新灯具太难了,那就是一个标志 代码可能需要一些严肃的重构。另一种方法是创造 你的价值对象的建设者 - 有工具,包括 IDE插件,Lombok等。人们也可以创造有意义的 测试类路径中的工厂方法。
来自here的另一句话:
为简单的值对象编写模拟没有多大意义 (无论如何都应该是不可变的),只需创建一个实例并使用即可 它。不值得创建接口/实现对来控制返回哪些时间值,只需创建具有适当值的实例并使用它们。当一堂课不值得嘲笑时,有几种启发式方法。首先,它只有访问器或简单的方法来处理它所拥有的值,它没有任何有趣的行为。其次,你不能想到除了VideoImpl之外的其他类的有意义的名称或一些这样模糊的术语。
这似乎是哑巴值对象只保留值的有效原因,但是当你有一个引用实体和其他值对象的ValueObject时,事情变得更加复杂。
让我们说我有Person和Pet对象,它们是实体和关系(所有者,医生等),它是两个人之间的ValueObject,并且具有RelationshipType,它也是一个Value Object。所以,关系基本上是:
class Relationship {
private Person person;
private Pet pet;
private RelationshipType type;
}
现在,让我们说我有一个像isOwnerRelationship,isDoctorRelationship等谓词的类。基本上谓词就像
一样简单关系 - > relationship.isOwner(); //委托给relationshipType.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));
}
当然这个例子过于简化,因为对于一个人你可能需要提供地址,对于Pet你可能必须提供BreedType,无论如何,即你可能需要提供的实体和价值对象的传递图可能非常巨大。当然,您可以模拟实体,但假设您在关系中有更多ValueObjects的ValueObjects。即使您拥有花哨的构建器,您也必须提供原始ValueObject的每个部分,即使单元测试仅测试它的单个方面。
在谓词测试中,如果谓词关心调用对象的一个特定方法或它们的组合,我为什么要关心完整的对象构造?
或者值对象是否可以被视为简单并且规则不适用?
答案 0 :(得分:1)
单元测试应该测试单个单元。因此,如果您的ValueObject
足够简单,那么它不应该影响SUT(被测试对象)的测试。但是,如果ValueObject
有复杂的行为,那么你应该嘲笑它。这简化了测试并将测试仅隔离到SUT。
答案 1 :(得分:1)
在谓词测试中,如果谓词关心调用对象的一个特定方法或它们的组合,我为什么要关心完整的对象构造?
如果谓词只关心调用一个特定方法,那么为什么要将整个值传递给它?
在测试驱动设计中,关键思想之一是编写测试是关于您正在创建的API的反馈;如果你发现API笨拙无法测试,那么它暗示API也可能使用起来很笨拙。
在这种特定情况下,测试试图告诉您当前的设计违反了interface segregation principle
不应该强迫任何客户依赖它不使用的方法。
所有谓词关心的都是对所有权的描述,所以也许这个想法应该在你的解决方案中明确表达
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
的任何拼写与您的本地编码风格一致。
总结
不要模拟价值对象
似乎是非常好的建议 - 它是一种启发式提醒您,为值对象创建模拟是解决错误的问题。
答案 2 :(得分:1)
我知道这个答案来得有点晚,但是,我认为您应该尝试使事情更简单。当对象创建不能给测试带来副作用时,请使用“真实的东西”(使用构造函数),例如,当您仅需要方法的某个返回值时,请使用模拟/存根。是否是值对象并不重要。
例如,值对象可以使用随机数生成器将值赋予其构造属性之一。这可能会对您的测试产生副作用(由于熵,在某些情况下可能不足以产生该数字),因此最好改用存根/模拟。
或者,如果您是一个完美主义者,并且想要对解决方案进行过度设计,则可以有一个简单值对象,然后将构造移至工厂类,将随机数生成器移至接口/基础结构类(域层中的IRandomNumberGenerator和RandomNumberGenerator在您的基础架构层中,这需要进行集成测试/压力测试,以查看您的随机性源的质量如何)。在这种情况下,您应该在测试中使用真实的东西,并在副作用已经转移到其他类时构造真实值对象。
应用KISS(保持简单愚蠢)规则。模拟以避免测试中的副作用,并只编写一行代码(当您只需要从方法中获得一定的回报时),否则使用真实的东西(通常比将这么多的方法存入更复杂的行为中要简单得多) )。
只需使用使您的代码更短,更简单,更易于遵循的方法,但请始终记住要小心可能带来副作用的对象。