构建的系统架构能让大多数测试用例不依赖于数据库就能执行,这样最好了,但是我们还是经常遇到许多测试用例需要数据库的。在这种情况下,我们可以扩展测试自动化框架(Test Automation Framework)去完成大部份的工作。可以增加一种使用框架来进行创建对象注册的办法,这样,框架就能为进行删除操作。
首先,当创建对象时,我们需要注册它。
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
注册过程由将对象加入到测试对象集合的操作构成:
List testObjects;
protected void setUp() throws Exception {
super.setUp();
testObjects = new ArrayList();
}
protected void registerTestObject(Object testObject) {
testObjects.add(testObject);
}
在tearDown方法中,我们遍历测试对象集合,并将每个对象删除:
public void tearDown() {
Iterator i = testObjects.iterator();
while (i.hasNext()) {
try {
deleteObject(i.next());
} catch (RuntimeException e) {
// Nothing to do; we just want to make sure
// we continue on to the next object in the list
}
}
}
现在测试用例变成这样子:
public void testAddItemQuantity_severalQuantity_v8(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
我们已经能够将try/finally语句块给移除。除了调用registerTestObject外,我们变得代码简单多了。但我们仍然可以进一步精简代码。为什么这样说?我们需要将声明变量,然后将它们初始化为null,再稍后对其重新初始化吗?之前的测试用例这样做是因为这样变量必须在finally语句块中可访问;现在我们将finally块移除了,所以我们可以将变量定义与初始化操作合并。
public void testAddItemQuantity_severalQuantity_v9(){
// Set up fixture
Address billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
Address shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(shippingAddress);
Customer customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
Product product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
Invoice invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.95"));
assertContainsExactlyOneLineItem(invoice, expected);
}
3) 清理夹具创建
我们已经清理好了断言及夹具卸载,现在来看看夹具创建。一个明显的“快速修复”就是,对构造函数和registerTestObject的调用,利用“方法抽取重构”来定义“生成方法”(Creation Method)。这样可以使得测试用例更易于读写。“生成方法”有另外一个好处:它们封装了SUT的API,使得当对象的构造函数发生改变时,我们不必每个测试用例都去更改,只需要去修改一个地方,减少了测试用例的维护成本。
public void testAddItemQuantity_severalQuantity_v10(){
// Set up fixture
Address billingAddress =
createAddress( "1222 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Address shippingAddress =
createAddress( "1333 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Customer customer =
createCustomer( 99, "John", "Doe", new BigDecimal("30"),
billingAddress, shippingAddress);
Product product =
createProduct( 88,"SomeWidget",new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product,5, new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
这个夹具创建逻辑还是有其他问题。第一个问题是,很难明确这个夹具与测试预期输出之前的联系。Customer对象的细节会以某种方式影响到输出吗?customer的address域会影响到输出?这个用例真正想验证的是什么?
另一个问题是:这个测试用例展示了“硬编码测试数据”(Hard-Coded Test Data)(见模糊测试)。如果SUT将我们创建的所有对象持久化到数据库中,那硬编码数据就会导致:当customer,product或者invoice的某些域要求必须唯一时,会出现“不可重复式测试”、“交互式测试”或者“测试执行冲突Test Run War”(见不稳定测试)。
我们可以通过为每一个测试用例生成唯一的值并用这个值做为种子(seed)去产生用例中使用到的对象。这个办法能保证每次用例执行时,都会得到不同的对象。因为我们已经将对象产生逻辑移到了生成方法中,这一步修改相对容易。我们只要将上述逻辑放到生成方法中并去掉相应的参数就行了。抽取方法式重构还有另外一个用处,我们可以生成一个新的、无参数的新版生成方法。
public void testAddItemQuantity_severalQuantity_v11(){
final int QUANTITY = 5;
// Set up fixture
Address billingAddress = createAnAddress();
Address shippingAddress = createAnAddress();
Customer customer = createACustomer(new BigDecimal("30"),
billingAddress, shippingAddress);
Product product = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5, new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
private Product createAProduct(BigDecimal unitPrice) {
BigDecimal uniqueId = getUniqueNumber();
String uniqueString = uniqueId.toString();
return new Product(uniqueId.toBigInteger().intValue(),
uniqueString, unitPrice);
}
我们将这个模式称为“匿名生成方法”(Anonymous Creation Method),因为这个模式表明我们并不关心对象本身的特性。如果SUT的预期行为依赖于特定的值,我们要么可以将这个值做为参数传到生成函数,要么可以在生成函数的函数名中暗示。
这个测试用例看上会好一些了,但是仍然没有做完。预期结果真的以某种方式依赖于customer对象的address?如果不依赖,我们可以通过抽取方法式重构(再一次)将它们的创建过程完全隐藏,这里用createACustomer方法来达到这个目的:
public void testAddItemQuantity_severalQuantity_v12(){
// Set up fixture
Customer cust = createACustomer(new BigDecimal("30"));
Product prod = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(cust);
// Exercise SUT
invoice.addItemQuantity(prod, 5);
// Verify outcome
LineItem expected = new LineItem(invoice, prod, 5,
new BigDecimal("30"), new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
将创建address对象的调用移到customer的创建方法中,我们更加清楚:这个测试用例中,address对象并不会影响我们需要验证的逻辑。然而,结果依赖于customer的discount,所以将discount做为customer的生成函数的参数。
我们还是有一两个地方要处理的。比如,表示单价(unit price),数量(quantity),折扣(discount)的测试数据是硬编码的,且在这个测试用例中重复出现了两次。我们可以使用“用符号化常量替换魔数式重构”来给这些数据以名称,用于说明它们的含意。另外,用于创建LineItem对象的构造函数并没有在SUT本身的其他地方使用到,因为LineItem只是在其创建时去计算extenedCost。我们可以将这个针对测试本身的代码放到测试辅助中的外部方法(Foreign Method[Fowler])。我们已经在处理Cuostom及Product中看到了例子:使用参数化的生成方法,只根据相关的值返回预期的的LineItem对象。
public void testAddItemQuantity_severalQuantity_v13(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal EXTENDED_PRICE = new BigDecimal("69.96");
LineItem expected =
new LineItem(invoice, product, QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE);
assertContainsExactlyOneLineItem(invoice, expected);
}
最后一点,69.96是从什么地方来的?如果这个值是从相关系统的输出中得到,是可以这样写,因为它只是手工计算出来并写入到用例中,我们可以为了测试阅读者方便,将计算过程在用例中展现出来。
4) 清理后的测试用例
下面是清理后的最终版本的测试用例:
public void testAddItemQuantity_severalQuantity_v14(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected =
createLineItem(QUANTITY, CUST_DISCOUNT_PC,
EXTENDED_PRICE, product, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
我们已经使用了“引入解释变量式重构”将BASE_PRICE(price*quantity)和EXTENDED_PRICE(带折扣的价格)的计算过程更好地文档化了。修订过的测试用例比我们开始时面对的笨重的代码各小巧、各清晰。它也实现了“测试即文档”的任务。那么,我们能发现这个测试验证的是什么吗?它验证了通过添加函数,商品(Item)确实回到了发货单(Invoice),而且总成本(extended cost)是基于产品价格(product price),用户的折扣(coustomer’s discount)及预定的数量(quantity ordered)。
0.4 写更多的测试用例
看上去我们花了很大的精力去重构这个用例以使其更加清晰,是不是每一个用例都要花费这么多的精力呢?
希望不是!这儿花费的努力大多数与发现用例中需要用到的测试辅助方法(Test Utility Methods)有关。我们定义了测试我们程序的“更高级语言”(Higher-Level Language)。一旦有了这些辅助方法,编写其他用例就会变得简单得多。比如,如果我们想去验证当lineItem的数量发生变化时,总价格(extended cost)会被重新计算,我们就可以重用大多数的测试辅助方法。
public void testAddLineItem_quantityOne(){
final BigDecimal BASE_PRICE = UNIT_PRICE;
final BigDecimal EXTENDED_PRICE = BASE_PRICE;
// Set up fixture
Customer customer = createACustomer(NO_CUST_DISCOUNT);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, QUAN_ONE);
// Verify outcome
LineItem expected =
createLineItem( QUAN_ONE, NO_CUST_DISCOUNT,
EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
public void testChangeQuantity_severalQuantity(){
final int ORIGINAL_QUANTITY = 3;
final int NEW_QUANTITY = 5;
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Invoice invoice = createInvoice(customer);
Product product = createAProduct( UNIT_PRICE);
invoice.addItemQuantity(product, ORIGINAL_QUANTITY);
// Exercise SUT
invoice.changeQuantityForProduct(product, NEW_QUANTITY);
// Verify outcome
LineItem expected = createLineItem( NEW_QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
这个用例只要了大概两分钟,而且没有去添加任何新的测试辅助方法。比较一下,如果按照之前的风格来写一个全新的测试用例,会要多长的时间。另外,在测试用例编写上面节省的精力只是等式的一部份,还应该考虑到,每次重访已有测试用例时,可以节省理解的时间。在项目开发及后续的维护活动中,这种节省还会是不断累积的。
0.5 进一步的精简
后来添加的测试用例带来了代码重复。比如,我们总会去创建Customer和Invoice对象。为什么不能将这两行合并呢?同理,我们在测试方法中不断定义、初始化常量QUANTITY及CUSTOMER_DISCOUNT_PC。为什么不能只做一次?Pruduct对象没有在测试中发挥作用,我们总是用同样方式去创建它。能将这个职责提出来吗?当然可以,我们对每组重复代码使用“抽取方法式重构”,创建一个更强大的“生成方法”。
public void testAddItemQuantity_severalQuantity_v15(){
// Set up fixture
Invoice invoice = createCustomerInvoice(CUST_DISCOUNT_PC);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, SEVERAL);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(SEVERAL));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected = createLineItem( SEVERAL,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
public void testAddLineItem_quantityOne_v2(){
final BigDecimal BASE_PRICE = UNIT_PRICE;
final BigDecimal EXTENDED_PRICE = BASE_PRICE;
// Set up fixture
Invoice invoice = createCustomerInvoice(NO_CUST_DISCOUNT);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, QUAN_ONE);
// Verify outcome
LineItem expected = createLineItem( SEVERAL,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
public void testChangeQuantity_severalQuantity_V2(){
final int NEW_QUANTITY = SEVERAL + 2;
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
// Set up fixture
Invoice invoice = createCustomerInvoice(CUST_DISCOUNT_PC);
invoice.addItemQuantity(PRODUCT, SEVERAL);
// Exercise SUT
invoice.changeQuantityForProduct(PRODUCT, NEW_QUANTITY);
// Verify outcome
LineItem expected = createLineItem( NEW_QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
我们将需要去理解的35行代码减少到6行。只需要去维护原有代码的1/6。可以进一步将夹具的创建放到setup方法中,不过只有将很多测试用例都需要同样的customer/Discout/Invoice的配置。如果我们想让其他测试用例类(Testcase Classes)复用这样测试辅助方法,我们可以使用“抽取超类式重构”创建一个测试用例超类(Testcase Superclass),再使用“上移方法式重构”将这些测试辅助函数上移,那它们就能被重用了。
首先,当创建对象时,我们需要注册它。
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
注册过程由将对象加入到测试对象集合的操作构成:
List testObjects;
protected void setUp() throws Exception {
super.setUp();
testObjects = new ArrayList();
}
protected void registerTestObject(Object testObject) {
testObjects.add(testObject);
}
在tearDown方法中,我们遍历测试对象集合,并将每个对象删除:
public void tearDown() {
Iterator i = testObjects.iterator();
while (i.hasNext()) {
try {
deleteObject(i.next());
} catch (RuntimeException e) {
// Nothing to do; we just want to make sure
// we continue on to the next object in the list
}
}
}
现在测试用例变成这样子:
public void testAddItemQuantity_severalQuantity_v8(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
我们已经能够将try/finally语句块给移除。除了调用registerTestObject外,我们变得代码简单多了。但我们仍然可以进一步精简代码。为什么这样说?我们需要将声明变量,然后将它们初始化为null,再稍后对其重新初始化吗?之前的测试用例这样做是因为这样变量必须在finally语句块中可访问;现在我们将finally块移除了,所以我们可以将变量定义与初始化操作合并。
public void testAddItemQuantity_severalQuantity_v9(){
// Set up fixture
Address billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
Address shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(shippingAddress);
Customer customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
Product product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
Invoice invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.95"));
assertContainsExactlyOneLineItem(invoice, expected);
}
3) 清理夹具创建
我们已经清理好了断言及夹具卸载,现在来看看夹具创建。一个明显的“快速修复”就是,对构造函数和registerTestObject的调用,利用“方法抽取重构”来定义“生成方法”(Creation Method)。这样可以使得测试用例更易于读写。“生成方法”有另外一个好处:它们封装了SUT的API,使得当对象的构造函数发生改变时,我们不必每个测试用例都去更改,只需要去修改一个地方,减少了测试用例的维护成本。
public void testAddItemQuantity_severalQuantity_v10(){
// Set up fixture
Address billingAddress =
createAddress( "1222 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Address shippingAddress =
createAddress( "1333 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Customer customer =
createCustomer( 99, "John", "Doe", new BigDecimal("30"),
billingAddress, shippingAddress);
Product product =
createProduct( 88,"SomeWidget",new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product,5, new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
这个夹具创建逻辑还是有其他问题。第一个问题是,很难明确这个夹具与测试预期输出之前的联系。Customer对象的细节会以某种方式影响到输出吗?customer的address域会影响到输出?这个用例真正想验证的是什么?
另一个问题是:这个测试用例展示了“硬编码测试数据”(Hard-Coded Test Data)(见模糊测试)。如果SUT将我们创建的所有对象持久化到数据库中,那硬编码数据就会导致:当customer,product或者invoice的某些域要求必须唯一时,会出现“不可重复式测试”、“交互式测试”或者“测试执行冲突Test Run War”(见不稳定测试)。
我们可以通过为每一个测试用例生成唯一的值并用这个值做为种子(seed)去产生用例中使用到的对象。这个办法能保证每次用例执行时,都会得到不同的对象。因为我们已经将对象产生逻辑移到了生成方法中,这一步修改相对容易。我们只要将上述逻辑放到生成方法中并去掉相应的参数就行了。抽取方法式重构还有另外一个用处,我们可以生成一个新的、无参数的新版生成方法。
public void testAddItemQuantity_severalQuantity_v11(){
final int QUANTITY = 5;
// Set up fixture
Address billingAddress = createAnAddress();
Address shippingAddress = createAnAddress();
Customer customer = createACustomer(new BigDecimal("30"),
billingAddress, shippingAddress);
Product product = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5, new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
private Product createAProduct(BigDecimal unitPrice) {
BigDecimal uniqueId = getUniqueNumber();
String uniqueString = uniqueId.toString();
return new Product(uniqueId.toBigInteger().intValue(),
uniqueString, unitPrice);
}
我们将这个模式称为“匿名生成方法”(Anonymous Creation Method),因为这个模式表明我们并不关心对象本身的特性。如果SUT的预期行为依赖于特定的值,我们要么可以将这个值做为参数传到生成函数,要么可以在生成函数的函数名中暗示。
这个测试用例看上会好一些了,但是仍然没有做完。预期结果真的以某种方式依赖于customer对象的address?如果不依赖,我们可以通过抽取方法式重构(再一次)将它们的创建过程完全隐藏,这里用createACustomer方法来达到这个目的:
public void testAddItemQuantity_severalQuantity_v12(){
// Set up fixture
Customer cust = createACustomer(new BigDecimal("30"));
Product prod = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(cust);
// Exercise SUT
invoice.addItemQuantity(prod, 5);
// Verify outcome
LineItem expected = new LineItem(invoice, prod, 5,
new BigDecimal("30"), new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
将创建address对象的调用移到customer的创建方法中,我们更加清楚:这个测试用例中,address对象并不会影响我们需要验证的逻辑。然而,结果依赖于customer的discount,所以将discount做为customer的生成函数的参数。
我们还是有一两个地方要处理的。比如,表示单价(unit price),数量(quantity),折扣(discount)的测试数据是硬编码的,且在这个测试用例中重复出现了两次。我们可以使用“用符号化常量替换魔数式重构”来给这些数据以名称,用于说明它们的含意。另外,用于创建LineItem对象的构造函数并没有在SUT本身的其他地方使用到,因为LineItem只是在其创建时去计算extenedCost。我们可以将这个针对测试本身的代码放到测试辅助中的外部方法(Foreign Method[Fowler])。我们已经在处理Cuostom及Product中看到了例子:使用参数化的生成方法,只根据相关的值返回预期的的LineItem对象。
public void testAddItemQuantity_severalQuantity_v13(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal EXTENDED_PRICE = new BigDecimal("69.96");
LineItem expected =
new LineItem(invoice, product, QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE);
assertContainsExactlyOneLineItem(invoice, expected);
}
最后一点,69.96是从什么地方来的?如果这个值是从相关系统的输出中得到,是可以这样写,因为它只是手工计算出来并写入到用例中,我们可以为了测试阅读者方便,将计算过程在用例中展现出来。
4) 清理后的测试用例
下面是清理后的最终版本的测试用例:
public void testAddItemQuantity_severalQuantity_v14(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected =
createLineItem(QUANTITY, CUST_DISCOUNT_PC,
EXTENDED_PRICE, product, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
我们已经使用了“引入解释变量式重构”将BASE_PRICE(price*quantity)和EXTENDED_PRICE(带折扣的价格)的计算过程更好地文档化了。修订过的测试用例比我们开始时面对的笨重的代码各小巧、各清晰。它也实现了“测试即文档”的任务。那么,我们能发现这个测试验证的是什么吗?它验证了通过添加函数,商品(Item)确实回到了发货单(Invoice),而且总成本(extended cost)是基于产品价格(product price),用户的折扣(coustomer’s discount)及预定的数量(quantity ordered)。
0.4 写更多的测试用例
看上去我们花了很大的精力去重构这个用例以使其更加清晰,是不是每一个用例都要花费这么多的精力呢?
希望不是!这儿花费的努力大多数与发现用例中需要用到的测试辅助方法(Test Utility Methods)有关。我们定义了测试我们程序的“更高级语言”(Higher-Level Language)。一旦有了这些辅助方法,编写其他用例就会变得简单得多。比如,如果我们想去验证当lineItem的数量发生变化时,总价格(extended cost)会被重新计算,我们就可以重用大多数的测试辅助方法。
public void testAddLineItem_quantityOne(){
final BigDecimal BASE_PRICE = UNIT_PRICE;
final BigDecimal EXTENDED_PRICE = BASE_PRICE;
// Set up fixture
Customer customer = createACustomer(NO_CUST_DISCOUNT);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, QUAN_ONE);
// Verify outcome
LineItem expected =
createLineItem( QUAN_ONE, NO_CUST_DISCOUNT,
EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
public void testChangeQuantity_severalQuantity(){
final int ORIGINAL_QUANTITY = 3;
final int NEW_QUANTITY = 5;
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Invoice invoice = createInvoice(customer);
Product product = createAProduct( UNIT_PRICE);
invoice.addItemQuantity(product, ORIGINAL_QUANTITY);
// Exercise SUT
invoice.changeQuantityForProduct(product, NEW_QUANTITY);
// Verify outcome
LineItem expected = createLineItem( NEW_QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
这个用例只要了大概两分钟,而且没有去添加任何新的测试辅助方法。比较一下,如果按照之前的风格来写一个全新的测试用例,会要多长的时间。另外,在测试用例编写上面节省的精力只是等式的一部份,还应该考虑到,每次重访已有测试用例时,可以节省理解的时间。在项目开发及后续的维护活动中,这种节省还会是不断累积的。
0.5 进一步的精简
后来添加的测试用例带来了代码重复。比如,我们总会去创建Customer和Invoice对象。为什么不能将这两行合并呢?同理,我们在测试方法中不断定义、初始化常量QUANTITY及CUSTOMER_DISCOUNT_PC。为什么不能只做一次?Pruduct对象没有在测试中发挥作用,我们总是用同样方式去创建它。能将这个职责提出来吗?当然可以,我们对每组重复代码使用“抽取方法式重构”,创建一个更强大的“生成方法”。
public void testAddItemQuantity_severalQuantity_v15(){
// Set up fixture
Invoice invoice = createCustomerInvoice(CUST_DISCOUNT_PC);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, SEVERAL);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(SEVERAL));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected = createLineItem( SEVERAL,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
public void testAddLineItem_quantityOne_v2(){
final BigDecimal BASE_PRICE = UNIT_PRICE;
final BigDecimal EXTENDED_PRICE = BASE_PRICE;
// Set up fixture
Invoice invoice = createCustomerInvoice(NO_CUST_DISCOUNT);
// Exercise SUT
invoice.addItemQuantity(PRODUCT, QUAN_ONE);
// Verify outcome
LineItem expected = createLineItem( SEVERAL,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
public void testChangeQuantity_severalQuantity_V2(){
final int NEW_QUANTITY = SEVERAL + 2;
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
// Set up fixture
Invoice invoice = createCustomerInvoice(CUST_DISCOUNT_PC);
invoice.addItemQuantity(PRODUCT, SEVERAL);
// Exercise SUT
invoice.changeQuantityForProduct(PRODUCT, NEW_QUANTITY);
// Verify outcome
LineItem expected = createLineItem( NEW_QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);
assertContainsExactlyOneLineItem( invoice, expected );
}
我们将需要去理解的35行代码减少到6行。只需要去维护原有代码的1/6。可以进一步将夹具的创建放到setup方法中,不过只有将很多测试用例都需要同样的customer/Discout/Invoice的配置。如果我们想让其他测试用例类(Testcase Classes)复用这样测试辅助方法,我们可以使用“抽取超类式重构”创建一个测试用例超类(Testcase Superclass),再使用“上移方法式重构”将这些测试辅助函数上移,那它们就能被重用了。
相关推荐
在本案例中,我们看到三个不同阶段的代码文件,分别对应重构过程的不同步骤:原始代码(01.Original.zip)、添加第一个测试用例(02.FirstTestAdded.zip)和添加四个测试用例(03.FourTestAdded.zip)。这为我们提供...
1. **编写测试**:为每个待测试的函数或方法创建一个对应的测试用例,通常一个测试用例对应一种预期的行为。 2. **设置环境**:在测试开始前,可能需要初始化数据或配置环境,如创建数据库连接、模拟依赖等。 3. **...
"测试金字塔"是一个结构化测试组织的原则,提倡底层更多的单元测试,中间层的集成测试,以及较少的端到端测试。这有助于保持测试速度并降低维护成本。 "测试优先级与分类"模式强调根据业务价值和风险来确定测试的...
1. **红**:首先,编写一个失败的单元测试,即一个测试用例,这个用例对应于要实现的功能。测试通常会因为缺少相应的功能代码而失败。 2. **绿**:然后,编写最小的代码量,使测试通过。这通常是一个简单的实现,只...
- **重要性**:确保每次测试都在一个干净的环境中运行,避免资源泄露或状态污染对后续测试的影响。 ##### 4. Back Door Manipulation (后门操作) - **定义**:通过非正常途径(如直接数据库访问)设置测试环境或验证结果...
单元测试作为软件开发过程中的一个重要组成部分,能够帮助开发者确保代码的质量,并在重构过程中保持系统的稳定性。本文档基于葛亮2013年7月4日的分享内容进行深入探讨,旨在通过解析单元测试的基本概念、目的及实践...
- **引入参数对象(Introduce Parameter Object)**:当一个方法接受多个参数时,可以创建一个新的类来封装这些参数,提高代码的可读性和可维护性。 #### 五、重构工具 随着技术的发展,现在有许多工具可以帮助...
《软件重构讲义》是关于软件开发中重构技术的一份资料,主要涵盖了重构的基本概念、目的、...这份资料为开发者提供了一个深入理解重构概念和技术的框架,对于提升个人和团队的开发效率及软件质量具有重要的指导价值。
二维二进小波的快速分解与重构算法matlab实现-ex7-4.rar 使用matlab来实现非正交二次样条二维二进小波的快速分解和重构 编程实现例7.4中可分离二维二进小波的快速分解与重构算法 算法实现 使用matlab来实现...
2. 内联函数(Inline Method):如果一个函数只在一个地方被调用,可以将其内容直接替换到调用位置,减少层次,提高效率。 3. 将类的职责分离(Split Class):如果一个类承担了过多职责,应将其拆分为多个更专注的...
UT(Unit Testing)工程是软件开发过程中的一个重要阶段,它涉及到对软件代码的最小可测试单元进行独立验证,以确保每个功能模块的正确性。这篇总结将深入探讨在Vector Cast环境下进行UT工程时可能遇到的问题以及...
1. **信号生成**:首先生成一维或二维的测试信号,如随机信号、正弦波、图像等。 2. **稀疏表示**:找到合适的基(如DCT、Wavelet)使得信号在该基下呈现稀疏性。 3. **测量矩阵**:设计测量矩阵,通常选择高斯矩阵...
综上所述,"react-旨在重构一个react事例"的项目涵盖了React组件化开发、Flux架构的应用以及代码重构的实践。通过对这个项目的学习,你可以深入理解React和状态管理,并了解如何通过重构提升代码质量和应用程序的...
4. 构筑测试体系:包括自测试代码的价值、待测试的示例代码、第一个测试、再添加一个测试、修改测试夹具、探测边界条件、测试远不止如此等。 5. 重构名录:包括重构的记录格式、挑选重构的依据等。 6. 第一组重构:...
### Matlab将二维图像三维重构 #### 知识点解析 在本知识点中,我们将探讨如何使用Matlab将一张二维图像转换为三维模型的过程。通过分析提供的代码片段,我们可以了解到实现这一转换的基本步骤。 #### 图像读取与...
小波变换是信号处理领域中的一个重要技术,能够对信号进行多尺度分解和重构。本文将介绍 Matlab 实现一维和二维离散小波变换,以及小波的重构。 一维离散小波变换 小波变换是一种多尺度分解技术,能够将信号分解成...
4. **Ant**:Ant是Apache软件基金会的一个项目,是一个基于Java的构建工具。它使用XML来定义构建过程,包括编译、打包、测试和部署等任务。与传统的Makefile相比,Ant更具有平台无关性,易于理解和使用,尤其在大型...
首先,重构应该是一个持续的过程,而不是一次性的大工程。其次,应该保持频繁的测试,确保代码重构不会引入新的错误。重构还应该采取小步快跑的方式,每次只进行少量改动,并且频繁地进行代码审查和测试,从而确保...