重构一个UT测试(二)[翻译]

上一篇 / 下一篇  2011-01-11 23:31:05 / 个人分类:测试代码编写

2)   清理夹具卸载逻辑

我们已经清理了结果验证逻辑,现在将注意力转向测试用例末尾的finally语句块。这些代码是做什么的?

     } finally {

        // Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

     }

大多数现代语言都有类似的tyr/finally结构,这些结构用来保证即使有错误或者异常发生时,某些代码还是会被执行。在一个“测试方法”中,finally语句块用于保证不管测试用例通过或不通过,那些清理用的代码都会执行到。一个失败断言会抛出一个异常,它会将执行控制交回“测试自动框架”[Test Automatin Framework]的异常处理代码,所以,我们使用finally块先去执行清理操作。这种方式让我们不再需要先捕获异常,再将其重新抛出。

这个测试用例中,finally语句块对测试中创建的每一个对象调用了deleteObject方法。很不幸,代码有一个致命的缺陷,你注意到了吗?

问题出在卸载过程本身。如果第一次调用deleteObject时抛出一个异常,会发生什么?正如代码所示那样,其他deleteObject调用不会被执行。解决方法是使用一个嵌套的try/finally语句将第一个deleteObject给包起来,这样可以保证第二个deleteObject调用总会被执行。但是,如果第二个调用失败了呢?这个例子中,我们需要总共6层的try/finally嵌套行。那将会使得测试用例的长度翻倍,我们不可能在第一个测试用例中编写和维护这么多代码。

     } finally {

        //     Teardown

        try {

           deleteObject(invoice);

        } finally {

           try {

              deleteObject(product);

           } finally {

              try {

                 deleteObject(customer);

              } finally {

                 try {

                    deleteObject(billingAddress);

                 } finally {

                    deleteObject(shippingAddress);

                 }

              }

           }

        }

问题出在我们现在有了一个“复杂卸载”(见含糊测试Obscure Test),要如何使得这段代码变好?我们怎么去测试这些测试代码?很明显,现在的办法并不十分有效。

当然,我们可以将这些代码移到tearDown方法中去,这样就能将它们从“测试方法”中移出。另外,由于tearDown方法的作用类似finally语句块,我们就能摆脱最外层的try/finally。遗憾的是,这个策略并没有触及到问题的根本:需要在每个测试用例中编写细节化的卸载代码。

我们可能通过使用“共享夹具”的方法一开始就避免创建对象,这样就不用在每个用例执行切换过程中去将对象销毁。不过,此方法会带来不少测试坏味,包括“不可重复式测试(Unrepeatable Test)”(见不稳定测试Erratic Test)、由于夹具共享引起交互的“交互式测试(Interacting Test)”。另外,共享夹具中对象的引用常常导致“神秘访客(Mystery Guests)”(见含糊测试)

最好的解决方案是使用“新鲜夹具”,同时避免为每个用例编写卸载代码。为达到这个目的,我们可以使用能被自动垃圾回收掉的内存中夹具。然而,当创建的对象是持久化时(如,他们被保存到数据库中),这个办法就失效了。

构建的系统架构能让大多数测试用例不依赖于数据库就能执行,这样最好了,但是我们还是经常遇到许多测试用例需要数据库的。在这种情况下,我们可以扩展测试自动化框架(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)。这样可以使得测试用例更易于读写。“生成方法”有另外一个好处:它们封装了SUTAPI,使得当对象的构造函数发生改变时,我们不必每个测试用例都去更改,只需要去修改一个地方,减少了测试用例的维护成本。

  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对象的细节会以某种方式影响到输出吗?customeraddress域会影响到输出?这个用例真正想验证的是什么?

另一个问题是:这个测试用例展示了“硬编码测试数据”(Hard-Coded Test Data)(见模糊测试)。如果SUT将我们创建的所有对象持久化到数据库中,那硬编码数据就会导致:customerproduct或者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

TAG:

 

评分:0

我来说两句

coolas

coolas

用心测试

日历

« 2024-04-29  
 123456
78910111213
14151617181920
21222324252627
282930    

数据统计

  • 访问量: 13834
  • 日志数: 21
  • 建立时间: 2007-08-31
  • 更新时间: 2018-06-12

RSS订阅

Open Toolbar