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

上一篇 / 下一篇  2011-01-05 13:18:51 / 个人分类:测试代码编写

       首先想骂一下《XUint 测试模式 -测试码重构》这本书的译者。多好一本书被不负责任的翻译给糟蹋了。 现在将最前面一部份试着翻译一下,希望能对写好UT有所帮助:

0.1             为什么要重构测试?

测试【这里指测试代码或用例】会迅速成为敏捷开发过程的瓶颈。对于从来没有体会过简单、易于理解的测试代码与复杂、迟钝、难以维护的测试代码之间区别的人来说,这可能不会马上显而易见。生产效率的差别会让人大吃一惊。

        本书的这部份会作为全书的一个“激发式例子”,它将给你展示重构测试代码能够带来多大的改变。这个例子将会从一个复杂的测试用例开始,一步步地,将它重构为简单而易懂的测试用例。在这个过程中,我将指出一些关键的坏味(smells)以及用于去除它们的模式。希望能提起你更多的胃口。

0.2             一个复杂的测试用例

这儿有一个我在多个项目到测试用例中经常出现的一类。

public void testAddItemQuantity_severalQuantity_v1(){

     Address billingAddress = null;

     Address shippingAddress = null;

     Customer customer = null;

     Product product = null;

     Invoice invoice = null;

     try {

        //  Set up fixture

        billingAddress = new Address("1222 1st St SW",

              "Calgary", "Alberta", "T2N 2V2","Canada");

        shippingAddress = new Address("1333 1st St SW",

              "Calgary", "Alberta", "T2N 2V2", "Canada");

        customer = new Customer(99, "John", "Doe",

                                new BigDecimal("30"),

                                billingAddress,

                                shippingAddress);

        product = new Product(88, "SomeWidget",

                              new BigDecimal("19.99"));

        invoice = new Invoice(customer);

        // Exercise SUT

        invoice.addItemQuantity(product, 5);

        // Verify outcome

        List lineItems = invoice.getLineItems();

        if (lineItems.size() == 1) {

           LineItem actItem = (LineItem) lineItems.get(0);

           assertEquals("inv", invoice, actItem.getInv());

           assertEquals("prod", product, actItem.getProd());

           assertEquals("quant", 5, actItem.getQuantity());

           assertEquals("discount", new BigDecimal("30"),

                          actItem.getPercentDiscount());

           assertEquals("unit price",new BigDecimal("19.99"),

                             actItem.getUnitPrice());

           assertEquals("extended", new BigDecimal("69.96"),

                          actItem.getExtendedPrice());

        } else {

           assertTrue("Invoice should have 1 item", false);

        }

     } finally {

        // Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

     }

  }

这个用例有点长,也比应有的情况更加地复杂。这个“晦涩测试”(page186)难以理解,因为测试用例中那么多行代码使得看清全貌很困难。它还有很多其他问题,我们会一一叙述。

0.3             清理测试用例

先让我们来看看测试用例的几个部份。

1)   清理验证逻辑

首先让关注验证预期结果的那部份。我们或许可以从断言去推断这个用例去验证的条件。

      List lineItems = invoice.getLineItems();

        if (lineItems.size() == 1) {

           LineItem actItem = (LineItem) lineItems.get(0);

           assertEquals("inv", invoice, actItem.getInv());

           assertEquals("prod", product, actItem.getProd());

           assertEquals("quant", 5, actItem.getQuantity());

           assertEquals("discount", new BigDecimal("30"),

                          actItem.getPercentDiscount());

           assertEquals("unit price",new BigDecimal("19.99"),

                             actItem.getUnitPrice());

           assertEquals("extended", new BigDecimal("69.96"),

                          actItem.getExtendedPrice());

        } else {

           assertTrue("Invoice should have 1 item", false);

        }

一个需要修复的简单问题就是最后一行那个愚钝的断言。调用带有false参数的assertTrue就是想使得测试断言失败,那么为什么不直接一点呢?将其改为对fail的调用:

     List lineItems = invoice.getLineItems();

     if (lineItems.size() == 1) {

        LineItem actItem = (LineItem) lineItems.get(0);

        assertEquals("inv", invoice, actItem.getInv());

        assertEquals("prod", product, actItem.getProd());

        assertEquals("quant", 5, actItem.getQuantity());

        assertEquals("discount", new BigDecimal("30"),

                       actItem.getPercentDiscount());

        assertEquals("unit price",new BigDecimal("19.99"),

                          actItem.getUnitPrice());

        assertEquals("extended", new BigDecimal("69.96"),

                       actItem.getExtendedPrice());

     } else {

        fail("Invoice should have exactly one line item");

     }

        可以把这个改动看成是“方法提取”重构(Fowler),因为我们将一个带硬编码参数的“陈述预期式断言”用一个意图更明确的“单一预期断言”去封装来调用。

        当然,这组断言还有更多的问题,比如,我们为什么需要这么多断言?结果是许多断言都在测试通过LineItem的构造函数设置的成员变量,而这些测试应该由其他的单元测试来覆盖。那么为什么要在这里重复这些断言呢?重复断言只会产生更多当逻辑发生变化时需要维护的代码。

        我们的解决办法是使用针对“预期对象”的一个简单的断言来代替针对对象每一个字段的断言。首先我们定义一个与预期结果类似的对象。在这个例子中,我们创建一个预期的LineItem对象,它的每个字段都是预期的值,包括通过product初始化好的unitPriceextenedPrice

        List lineItems = invoice.getLineItems();

        if (lineItems.size() == 1) {

           LineItem expected =

              new LineItem(invoice, product, 5,

                           new BigDecimal("30"),

                           new BigDecimal("69.96"));

           LineItem actItem = (LineItem) lineItems.get(0);

           assertEquals("invoice", expected.getInv(),

                                   actItem.getInv());

           assertEquals("product", expected.getProd(),

                                   actItem.getProd());

           assertEquals("quantity",expected.getQuantity(),

                                   actItem.getQuantity());

           assertEquals("discount",

                        expected.getPercentDiscount(),

                        actItem.getPercentDiscount());

           assertEquals("unit pr", new BigDecimal("19.99"),

                                   actItem.getUnitPrice());

           assertEquals("extend pr",new BigDecimal("69.96"),

                                    actItem.getExtendedPrice());

        } else {

           fail("Invoice should have exactly one line item");

        }

一旦我们创建了“预期结果对象”,我们就可以使用针对对象的assertEquals

      List lineItems = invoice.getLineItems();

      if (lineItems.size() == 1) {

         LineItem expected =

            new LineItem(invoice, product,5,

                         new BigDecimal("30"),

                         new BigDecimal("69.96"));

         LineItem actItem = (LineItem) lineItems.get(0);

         assertEquals("invoice", expected, actItem);

      } else {

         fail("Invoice should have exactly one line item");

      }

明显地,“保持对象完整”重构方法【Fowler】使得代码更加简洁。但是!为什么测试用例中会有if?如果一个用例中有多条路径分支,我们怎么知道它到底执行了哪个分支?最好能去掉这种“条件式测试逻辑”。幸运的是,“守卫式断言”这是用来处理此类情况。简单采用“卫语句代替条件表达式”重构方法【Fowler】,用针对相同条件的一个断言来代替if…else fail()语句序列。这种“守卫式断言”会在条件不满足时终止执行,而不用引入“条件式测试逻辑”。

     List lineItems = invoice.getLineItems();

     assertEquals("number of items", 1,lineItems.size());

     LineItem expected =

        new LineItem(invoice, product, 5,

                     new BigDecimal("30"),

                     new BigDecimal("69.96"));

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("invoice", expected, actItem);

        {52}至此,我们将11行验证语句缩减到4行,另外,这4行代码也更为简单(and those 4lines are a lot simpler code to bootto bootin addition to everything else you have mentioned)。(注:没有根据我们所写的代码行数来领取报酬,真是件好事啊!这也是为何说KLOC是生产效率糟糕度量的一个例子)。不少人认识这些重构已经足够好了,但是,我们还可以让这些断言更加明了吗?我们真正想验证的到底是什么?我们想说的是,只有一个lineItem,它应该与我们定义的expectedLineItem完全一致。我们可以使用“抽取方法”重构定义一个“自定义断言”来明白地将这种验证给表现出来。

        LineItem expected =

           new LineItem(invoice, product, 5,

                        new BigDecimal("30"),

                        new BigDecimal("69.96"));

        assertContainsExactlyOneLineItem(invoice, expected);

这样好多了!现在,测试用例的验证部份只有两行。当我们将用例完整地再看看:

  public void testAddItemQuantity_severalQuantity_v6(){

     Address billingAddress = null;

     Address shippingAddress = null;

     Customer customer = null;

     Product product = null;

     Invoice invoice = null;

     try {

        //  Set up fixture

        billingAddress = new Address("1222 1st St SW",

               "Calgary", "Alberta", "T2N 2V2", "Canada");

        shippingAddress = new Address("1333 1st St SW",

               "Calgary", "Alberta", "T2N 2V2", "Canada");

        customer = new Customer(99, "John", "Doe",

                                new BigDecimal("30"),

                                billingAddress,

                                shippingAddress);

        product = new Product(88, "SomeWidget",

                              new BigDecimal("19.99"));

        invoice = new Invoice(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);

     } finally {

        // Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

     }

  }

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 {

TAG:

 

评分:0

我来说两句

Open Toolbar