现在代码已经变得很简洁了,由于消除了 try-finally 语句的缘故,也不需要提前声明 billingAddress、customer ... 这些变量,所以代码可以更简洁:
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); } |
构建测试环境部分 - Setup Fixture
这部分代码的特点是每个对象被创建完之后都会被注册到框架中,一个简单快速的重构方案是创建一个 createXXX 方法来做这些事情:先创建对象再注册到框架中,额外的好处是如果对象的构造方法改变了,我们不需要去修改每个调用构造方法的地方:
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 的构造需要6个传入参数,但是我们无法判断哪一个参数才是被验证的,哪些参数是无关的,如果修改 Customer 的 Address 参数会不会影响测试结果,解决这个问题需要突出被测试的参数。
使用硬编码的测试数据,在构建 Customer 时在代码中写死了6个参数,每次执行测试这6个参数都是不变的,假如每次构建Customer时需要将对象持久化在数据库中,那么第二次执行测试时就会因为字段写入冲突而失败(假如Customer的name字段必须保持唯一),所以每次执行TestCase时对某些字段要随机化每次都生成不一样的字段。
由于我们已经把对象的创建拿到了createXXX方法中,所以可以很容易做下面的重构:
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(), } |
这种模式叫 Anonymous Creation Method 模式,把不重要的字段放在构造方法内部实现,把需要测试的字段通过方法的传入参数暴露出来。同样的道理,如果 Address 字段不影响测试,那么可以进一步的隐藏Address的构建:
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); } |
最后一个问题,我们使用了魔数,这在学C语言的时候,老师已经强调过。需要把这些数字替换成更有意义的符号:
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.6”的出现依然是个问题,无法判断这个数字是怎么得到的,如果它是通过某种计算得到的,应该在测试代码中体现这种行为,所以这才是最终版本的重构:
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); } |
总结
上面的重构把原来硕大的方法体修改到11行,测试代码简洁明了了很多。但是有时候我们不禁要问这样的重构值得吗?因为我们实际上是把更多的代码变成工具方法转移到了别的地方。如果仅仅只写了这一个TestCase,这种重构略显尴尬,如果还需要写更多的TestCase,重构会让之前的付出有所收获,比如添加更多的TestCase时,只需要:
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 ); } |