发布新日志

  • 上厕所、想测试Google Testing on the Toilet (TotT):测试行为,而不是方法【翻译】

    2014-04-15 06:20:14

    上厕所、想测试Google Testing on the Toilet (TotT):测试行为,而不是方法

    由Erik Kuefler Monday, April 14, 2014
    本文改编自谷歌测试上厕所(TOTT)插曲。你可以下载一个打印机友好的版本,这TOTT插曲并张贴在你的办公室。

       写完一个方法后,很容易写只是一个测试来验证这个方法所做的一切。但是,认为测试和公有方法应该存在1:1的关系,这是非常有害的。我们真正要测试的是行为,而一个方法可能表现出很多的行为,另外,一个单一的行为有时会跨越多个方法。 让我们来看看一个验证了整个方法的糟糕的测试: 

    @Test public void testProcessTransaction() {
      User user = newUserWithBalance(LOW_BALANCE_THRESHOLD.plus(dollars(2));
      transactionProcessor.processTransaction(
          user,
          new Transaction("Pile of Beanie Babies", dollars(3)));
      assertContains("You bought a Pile of Beanie Babies", ui.getText());
      assertEquals(1, user.getEmails().size());
      assertEquals("Your balance is low", user.getEmails().get(0).getSubject());
    }

    显示所购买物品的名称,并发送余额减少的电子邮件,是两个独立的行为,但这个测试同时考虑了这两个行为,只是因为他们碰巧用同样的方法来触发。这样测试往往成为大而重,而且随着时间的推移,将变得难以维护,因为有其他行为需要不断的补充,最终导致难以判断到底是输入的哪一部分决定了断言。该测试的名称是被测方法的名称的直接镜象,就是一个不好的迹象。 更好的主意是:使用单独的测试,以验证不同的行为: 

    @Test public void testProcessTransaction_displaysNotification() {
      transactionProcessor.processTransaction(
          new User(), new Transaction("Pile of Beanie Babies"));
      assertContains("You bought a Pile of Beanie Babies", ui.getText());
    }
    @Test public void testProcessTransaction_sendsEmailWhenBalanceIsLow() {
      User user = newUserWithBalance(LOW_BALANCE_THRESHOLD.plus(dollars(2));
      transactionProcessor.processTransaction(
          user,
          new Transaction(dollars(3)));
      assertEquals(1, user.getEmails().size());
      assertEquals("Your balance is low", user.getEmails().get(0).getSubject());
    }

    现在,当有人添加新的行为,他们会写针对该行为写新的测试用例。无论有多少行为被添加,每个测试用例将保持集中和易于理解。这会让你的测试用例更具弹性,因为增加新的行为的测试是不太可能破坏现有的测试;使得测试用例更清晰,因为每个用例只包含的测试一个行为的代码。 

    原文地址:http://googletesting.blogspot.com/2014/04/testing-on-toilet-test-behaviors-not.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+blogspot%2FRLXA+%28Google+Testing+Blog%29
  • 重构一个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 查看(592) 评论(0) 收藏 分享 管理

  • 重构一个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 boot to boot in 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 {

  • 测试代码的坏味(翻译)

    2010-03-04 23:03:20

    测试代码的坏味

    Refactoring Test Code. xp2001,Arie van Deursen.. 

        1.神秘的客人

        当测试用到外部资源,如包含测试数据的文件,这时测试不再是自包含的了。不再有足够的信息去理解这个测试的功能,测试例也很难用做说明文档。另外,使用外部资源引入了潜在的依赖。如果这些资源发生改变,如删除或修改,相关测试将失败。

        2.资源乐观主义

        测试代码对外部资源(如特定的目录或数据库表)所做的存在与否及所处状态的乐观假定,都可能使得测试结果的无法确定。测试用例时而通过,时而又神奇的能不过,这种情形是你所不想要的。

        3.测试运行战争

        此类战争发生在:当你独自一人执行测试,它能工作良好,但是是多个程序员一起执行时,测试就失败了。这常常是由于资源冲突造成的。

        4.过于通用的运行必备

        在JUnit框架中,程序员会将每个测试执行前用到的代码写成setUp方法,这样给这些测试创造了一个运行必备。
        当setUp运行必备太过通用,不同的测试用例只使用到必备中的一部分时,坏味就出现了。必备变得难以阅读和理解。更严重的是,它会使得测试运行慢很多。测试执行太长时间的危险在于测试工作开始干扰其他编程工作,最终使得程序员不再执行测试。

        5.过于热心的测试

        当一个测试方法检测一个被测对象的多个方法时,就变得难以阅读与理解,也很难当作文档。也使得测试相互依赖性更强,更难维护。

        6.懒惰测试

        这种情况发生在当多个测试方法使用同一个必备来检查同一个方法时。这些测试常常只有放在一起才是有意义的。

        7.赌轮盘似的断言

        这种坏味来自于在一个测试方法中有很多没有解释的断言。当一个断言失败,你不知道到底是哪一个失败了。

        8.间接测试

        当一个测试类包含了测试其他被测对象时(比如它们被被测对象引用)。

        9.只为测试人员

        当一个产品类包含只用于测试的方法,这些方法要么是不需要、可被删除的,要么只是用于准备测试必备。如果你不想它们存在产品代码中,你可以将其移出。

        10.过于敏感的相等

        使用toString方法可以快速简单地写出相等的检查。一个典型方法就是计算得到实际结果,将它映射到一个string,然后将其与代码预期结果的string进行比较。然而这种测试的结果可能会依赖于请多无关的细节,如逗号、引号、空格等。当对象的toString方法改变后,测试就会失败。

        11.测试代码重复

Open Toolbar