测试值对象
为了强调测试驱动,在实现值对象之前,让我们先来看看测试。通过模拟客户 端对值对象的使用,这些测试可以驱动出对领域模型的设计。
这里,我们所关心的并不仅仅是单元测试的各个方面,而是演示客户端是如何 使用我们的领域模型的。在设计领域模型时,从客户端的角度思考有助于捕获关键的领域概念。否则,我们便是在从自己的角度设计模型,而不是业务的角度。
最好是采用示例代码
我们可以这么看待这种測试风格:如果我们要为自己的模型编写一个用户手册,我们便 可以通过这些测试代码来展示客户端对领域模型的使用。.
当然,也不是说我们就不应该编写单元测试,对于团队所要求的所有类型的 测试,我们都应该完成。但是,每种测试的关注点是不同的。单元测试和行为测试有它们自己的关注点,而下面的模型测试则有另外的关注点。
这里我们选择的值对象来自于敏捷项目管理上下文的核心域(2),它是演示模 型测试的好例子。
在该限界上下文中.领域专家会说"待定项的业务优 先级"。为了建模这部分通用语言.我们创建了一个名为BusinessPriority的类。该类用于衡量每一个待定项的业务价值[Wiegers],它返回的是成本百分比,或者当前待定项与其他待定项的比较成本。同时.BusmessPHorhy还向外提供了开发某个待定项的总价值.或者开发当前待定项与其他待定项的比较价值。此外.该类还提供了当前待定项与其他待定项相比起来的业务优先级。
这些测试在开发的过程中需要逐步改进.最终的测试代码如下:
package com.saasovation.agilepm.domain.model.product;
import com.saasovation.agilepm.domain.model.DomainTest;
import java.text.NumberFormat;
public class BusinessPriorityTest extends DomainTest {
public BusinessPriorityTest() {
super();
}
...
private NumberFormat oneDecimal() {
return this.decimal(1);
}
private NumberFormat twoDecimals() {
return this.decimal(2);
}
private NumberFormat decimal(int aNumberOfDecimals) {
NumberFormat fmt = NumberFormat.getInstance();
fmt.setMinimumFractionDigits(aNumberOfDecimals);
fmt.setMaximumFractionDigits(aNumberOfDecimals);
return fmt;
}
}
该测试类中存在一些帮助函数。由于团队需要测试各个计算过程的精确性.他们提供了 —些NumberFomiat对象来确保计算结果中保留小数的数目:
public void testCostPercentageCalculation() throws Exception {
BusinessPriority businessPriority = new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
BusinessPriority businessPriorityCopy = new BusinessPriority(businessPriority);
assertEquals(businessPriority, businessPriorityCopy);
BusinessPriorityTotals totals = new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
float cost = businessPriority.costPercentage(totals);
assertEquals(this.oneDecimal().format(cost), "2.7");
assertEquals(businessPriority, businessPriorityCopy);
}
在测试值对象的不变性时,团队成员想出了一个好主意:每个测试首先创建一个 BusinessPriority实例.然后通过复制构造函数创建一个与之相等的复制实例。测试的第一个断言保证了复制构造函数所创建的实例和原来的实例是相等的。
接下来.创建一个BusinessPriorityTotals实例,并将其赋给totals变量。有了该变量,他们 便可以调用cosePercentage ()查询方法.然后将查询结果赋给cost变量。再验证cost的值为2.7.该值是他们手工计算出来的期望值。最后.他们需要验证costPercentage ()方法是无副作用的,即验证businessPriority和businessPriorityCopy依然是相等的。通过以上测试,团队成员便知道如何计算成本百分比.并且知道返回的结果是什么样子。
之后.他们需要测试优先级.总价值和价值百分比.他们可以使用和以上测试相同的测 试模板:
public void testPriorityCalculation() throws Exception {
BusinessPriority businessPriority = new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
BusinessPriority businessPriorityCopy = new BusinessPriority(businessPriority);
assertEquals(businessPriorityCopy, businessPriority);
BusinessPriorityTotals totals = new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
float calculatedPriority = businessPriority.priority(totals);
assertEquals("1.03", this.twoDecimals().format(calculatedPriority));
assertEquals(businessPriority, businessPriorityCopy);
}
public void testTotalValueCalculation() throws Exception {
BusinessPriority businessPriority = new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
BusinessPriority businessPriorityCopy = new BusinessPriority(businessPriority);
assertEquals(businessPriorityCopy, businessPriority);
BusinessPriorityTotals totals = new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
float calculatedPriority = businessPriority.priority(totals);
assertEquals("1.03", this.twoDecimals().format(calculatedPriority));
assertEquals(businessPriority, businessPriorityCopy);
}
public void testTotalValueCalculation() throws Exception {
BusinessPriority businessPriority = new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
BusinessPriority businessPriorityCopy = new BusinessPriority(businessPriority);
assertEquals(businessPriority, businessPriorityCopy);
float totalValue = businessPriority.totalValue();
assertEquals("6.0", this.oneDecimal().format(totalValue));
assertEquals(businessPriority, businessPriorityCopy);
}
public void testValuePercentageCalculation() throws Exception {
BusinessPriority businessPriority = new BusinessPriority(new BusinessPriorityRatings(2, 4, 1, 1));
BusinessPriority businessPriorityCopy = new BusinessPriority(businessPriority);
assertEquals(businessPriority, businessPriorityCopy);
BusinessPriorityTotals totals = new BusinessPriorityTotals(53, 49, 53 + 49, 37, 33);
float valuePercentage = businessPriority.valuePercentage(totals);
assertEquals("5.9", this.oneDecimal().format(valuePercentage));
assertEquals(businessPriorityCopy, businessPriority);
}
测试应该具有领域含义
在创建测试时,我们应该保证领域专家能够读懂这些测试,即测试应该具有领域含义。
在获得足够帮助的情况下,非技术的领域专家通过阅读以上的测试代码是能够理解 BusinessPriority的使用方式的,他们也能从中看出,BusinessPriority的确表达出了通用语言的意图。
重要的是,对值对象的每次操作都没有破坏它的不变性。客户代码可以计算任何数量 待定项的优先级、对它们进行排序、比较.或者调整每个待定的BusinessPriorityRatings。