trend 列单元格的绿色表明测试通过(例如,FIT 设置 value1 为 84.0,value2 为 71.2,调用 trend 得到返回值 “decreasing”)。
查看 FIT 运行
可以通过命令行,用 Ant 任务并通过 Maven 调用 FIT,从而简单地把 FIT 测试插入构建过程。因为自动进行 FIT 测试,就像 JUnit 测试一样,所以也可以定期运行它们,例如在持续集成系统中。
最简单的命令行运行器,如清单 2 所示,是 FIT 的 FolderRunner,它接受两个参数 —— 一个是 FIT 表格的位置,一个是结果写入的位置。不要忘记配置类路径!
清单 2. FIT 的命令行
%>java fit.runner.FolderRunner ./test/fit ./target/
|
FIT 通过插件,还可以很好地与 Maven 一起工作,如清单 3 所示。只要下载插件,运行 fit:fit 命令,就 OK 了!
清单 3. Maven 得到 FIT
C:\dev\proj\edoa>maven fit:fit
__ __
| \/ |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \ ~ intelligent projects ~
|_| |_\__,_|\_/\___|_||_| v. 1.0.2
build:start:
java:prepare-filesystem:
java:compile:
[echo] Compiling to C:\dev\proj\edoa/target/classes
java:jar-resources:
test:prepare-filesystem:
test:test-resources:
test:compile:
fit:fit:
[java] 2 right, 0 wrong, 0 ignored, 0 exceptions
BUILD SUCCESSFUL
Total time: 4 seconds
Finished at: Thu Feb 02 17:19:30 EST 2006
|
试用 FIT:案例研究
现在已经了解了 FIT 的基础知识,我们来做一个练习。如果还没有 下载 FIT,现在是下载它的时候了!如前所述,这个案例研究显示出可以容易地把 FIT 和 JUnit 测试组合在一起,形成多层质量保证。
假设现在要为一个酿酒厂构建一个订单处理系统。酿酒厂销售各种类型的酒类,但是它们可以组织成两大类:季节性的和全年性的。因为酿酒厂以批发方式运作,所以酒类销售都是按桶销售的。对于零售商来说,购买多桶酒的好处就是折扣,而具体的折扣根据购买的桶数和酒是季节性还是全年性的而不同。
麻烦的地方在于管理这些需求。例如,如果零售店购买了 50 桶季节性酒,就没有折扣;但是如果这 50 桶不是 季节性的,那么就有 12% 的折扣。如果零售店购买100 桶季节性酒,那就有折扣,但是只有 5%。100 桶更陈的非季节性酒的折扣达到 17%。购买量达到 200 时,也有类似的规矩。
对于开发人员,像这样的需求集可能让人摸不着头脑。但是请看,我们的啤酒-酿造行业分析师用 FIT 表可以很容易地描述出这个需求,如图 5 所示:
图 5. 我的业务需求非常清晰!

表格语义
这个表格从业务的角度来说很有意义,它确实很好地规划出需求。但是作为开发人员,还需要对表格的语言了解更多一些,以便从表格得到值。首先,也是最重要的,表格中的初始行说明表格的名称,它恰好与一个匹配的类对应(org.acme.store.discount.DiscountStructureFIT)。命名要求表格作者和开发人员之间的一些协调。至少,需要指定完全限定的表格名称(也就是说,必须包含包名,因为 FIT 要动态地装入对应的类)。
请注意表格的名称以 FIT 结束。第一个倾向可能是用 Test 结束它,但要是这么做,那么在自动环境中运行 FIT 测试和 JUnit 测试时,会与 JUnit 产生些冲突,JUnit 的类通常通过命名模式查找,所以最好避免用 Test 开始或结束 FIT 表格名称。
下一行包含五列。每个单元格中的字符串都特意用斜体格式,这是 FIT 的要求。前面学过,单元格名称与装备的实例成员和方法匹配。为了更简洁,FIT 假设任何值以括号结束的单元格是方法,任何值不以括号结束的单元格是实例成员。
特殊智能
FIT 在处理单元格的值,进行与对应装备类的匹配时,采用智能解析。如 图 5 所示,第二行单元格中的值是用普通的英文编写的,例如 “number of cases”。FIT 试图把这样的字符串按照首字母大写方式连接起来;例如,“number of cases” 变成 “numberOfCases”,然后 FIT 试图找到对应的装备类。这个原则也适用于方法 —— 如图 5 所示,“discount price()” 变成了 “discountPrice()”。
FIT 还会智能地猜测单元格中值的具体类型。例如,在 图 5 余下的八行中,每一列都有对应的类型,或者可以由 FIT 准确地猜出,或者要求一些定制编程。在这个示例中,图 5 有三种不同类型。与 “number of cases” 关联的列匹配到 int,而与 “is seasonal” 列关联的值则匹配成 boolean。
剩下的三列,“list price per case”、“discount price()” 和 “discount amount()” 显然代表当前值。这几列要求定制类型,我将把它叫作 Money。有了它之后,应用程序就要求一个代表钱的对象,所以在我的 FIT 装备中遵守少量语义就可以利用上这个对象!
FIT 语义总结
表 1 总结了命名单元格和对应的装备实例变量之间的关系:
表 1. 单元格到装备的关系:实例变量
| 单元格值 |
对应的装备实例变量 |
类型 |
| list price per case |
listPricePerCase |
Money |
| number of cases |
numberOfCases |
int |
| is seasonal |
isSeasonal |
boolean |
表 2 总结了 FIT 命名单元格和对应的装备方法之间的关系:
表 2. 单元格到装备的关系:方法
| 表格单元格的值 |
对应的装备方法 |
返回类型 |
| discount price() |
discountPrice |
Money |
| discount amount() |
discountAmount |
Money |
该构建了!
要为酿酒厂构建的订单处理系统有三个主要对象:一个 PricingEngine 处理包含折扣的业务规则,一个 WholeSaleOrder 代表订单,一个 Money 类型代表钱。
Money 类
第一个要编写的类是 Money 类,它有进行加、乘和减的方法。可以用 JUnit 测试新创建的类,如清单 14 所示:
清单 4. JUnit 的 MoneyTest 类
package org.acme.store;
import junit.framework.TestCase;
public class MoneyTest extends TestCase {
public void testToString() throws Exception{
Money money = new Money(10.00);
Money total = money.mpy(10);
assertEquals("$100.00", total.toString());
}
public void testEquals() throws Exception{
Money money = Money.parse("$10.00");
Money control = new Money(10.00);
assertEquals(control, money);
}
public void testMultiply() throws Exception{
Money money = new Money(10.00);
Money total = money.mpy(10);
Money discountAmount = total.mpy(0.05);
assertEquals("$5.00", discountAmount.toString());
}
public void testSubtract() throws Exception{
Money money = new Money(10.00);
Money total = money.mpy(10);
Money discountAmount = total.mpy(0.05);
Money discountedPrice = total.sub(discountAmount);
assertEquals("$95.00", discountedPrice.toString());
}
}
|
WholeSaleOrder 类
然后,定义 WholeSaleOrder 类型。这个新对象是应用程序的核心:如果 WholeSaleOrder 类型配置了桶数、每桶价格和产品类型(季节性或全年性),就可以把它交给 PricingEngine,由后者确定对应的折扣并相应地在 WholeSaleOrder 实例中配置它。
WholesaleOrder 类的定义如清单 5 所示:
清单 5. WholesaleOrder 类
package org.acme.store.discount.engine;
import org.acme.store.Money;
public class WholesaleOrder {
private int numberOfCases;
private ProductType productType;
private Money pricePerCase;
private double discount;
public double getDiscount() {
return discount;
}
public void setDiscount(double discount) {
this.discount = discount;
}
public Money getCalculatedPrice() {
Money totalPrice = this.pricePerCase.mpy(this.numberOfCases);
Money tmpPrice = totalPrice.mpy(this.discount);
return totalPrice.sub(tmpPrice);
}
public Money getDiscountedDifference() {
Money totalPrice = this.pricePerCase.mpy(this.numberOfCases);
return totalPrice.sub(this.getCalculatedPrice());
}
public int getNumberOfCases() {
return numberOfCases;
}
public void setNumberOfCases(int numberOfCases) {
this.numberOfCases = numberOfCases;
}
public void setProductType(ProductType productType) {
this.productType = productType;
}
public String getProductType() {
return productType.getName();
}
public void setPricePerCase(Money pricePerCase) {
this.pricePerCase = pricePerCase;
}
public Money getPricePerCase() {
return pricePerCase;
}
}
|
从清单 5 中可以看到,一旦在 WholeSaleOrder 实例中设置了折扣,就可以通过分别调用 getCalculatedPrice 和 getDiscountedDifference 方法得到折扣价格和节省的钱。
更好地测试这些方法(用 JUnit)!
定义了 Money 和 WholesaleOrder 类之后,还要编写 JUnit 测试来验证 getCalculatedPrice 和 getDiscountedDifference 方法的功能。测试如清单 6 所示:
清单 6. JUnit 的 WholesaleOrderTest 类
package org.acme.store.discount.engine.junit;
import junit.framework.TestCase;
import org.acme.store.Money;
import org.acme.store.discount.engine.WholesaleOrder;
public class WholesaleOrderTest extends TestCase {
/*
* Test method for 'WholesaleOrder.getCalculatedPrice()'
*/
public void testGetCalculatedPrice() {
WholesaleOrder order = new WholesaleOrder();
order.setDiscount(0.05);
order.setNumberOfCases(10);
order.setPricePerCase(new Money(10.00));
assertEquals("$95.00", order.getCalculatedPrice().toString());
}
/*
* Test method for 'WholesaleOrder.getDiscountedDifference()'
*/
public void testGetDiscountedDifference() {
WholesaleOrder order = new WholesaleOrder();
order.setDiscount(0.05);
order.setNumberOfCases(10);
order.setPricePerCase(new Money(10.00));
assertEquals("$5.00", order.getDiscountedDifference().toString());
}
}
|
PricingEngine 类
PricingEngine 类利用业务规则引擎,在这个示例中,是 Drools。PricingEngine 极为简单,只有一个 public 方法:applyDiscount。只要传递进一个 WholeSaleOrder 实例,引擎就会要求 Drools 应用折扣,如清单 7 所示:
清单 7. PricingEngine 类
package org.acme.store.discount.engine;
import org.drools.RuleBase;
import org.drools.WorkingMemory;
import org.drools.io.RuleBaseLoader;
public class PricingEngine {
private static final String RULES="BusinessRules.drl";
private static RuleBase businessRules;
private static void loadRules() throws Exception{
if (businessRules==null){
businessRules = RuleBaseLoader.
loadFromUrl(PricingEngine.class.getResource(RULES));
}
}
public static void applyDiscount(WholesaleOrder order) throws Exception{
loadRules();
WorkingMemory workingMemory = businessRules.newWorkingMemory( );
workingMemory.assertObject(order);
workingMemory.fireAllRules();
}
}
|
上一页 [1] [2] [3] 下一页

【责编:wayen】