`
coolas
  • 浏览: 6804 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
最近访客 更多访客>>
社区版块
存档分类
最新评论

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

阅读更多
    首先想骂一下《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初始化好的unitPrice和extenedPrice。
         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 {
               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)”(见含糊测试)。
最好的解决方案是使用“新鲜夹具”,同时避免为每个用例编写卸载代码。为达到这个目的,我们可以使用能被自动垃圾回收掉的内存中夹具。然而,当创建的对象是持久化时(如,他们被保存到数据库中),这个办法就失效了。(未完。。。。)
分享到:
评论

相关推荐

    《重构》C#版(一)构建参考结果的测试用例

    在本案例中,我们看到三个不同阶段的代码文件,分别对应重构过程的不同步骤:原始代码(01.Original.zip)、添加第一个测试用例(02.FirstTestAdded.zip)和添加四个测试用例(03.FourTestAdded.zip)。这为我们提供...

    UT的测试文档

    1. **编写测试**:为每个待测试的函数或方法创建一个对应的测试用例,通常一个测试用例对应一种预期的行为。 2. **设置环境**:在测试开始前,可能需要初始化数据或配置环境,如创建数据库连接、模拟依赖等。 3. **...

    XUNIT测试模式-测试码重构(英文)

    "测试金字塔"是一个结构化测试组织的原则,提倡底层更多的单元测试,中间层的集成测试,以及较少的端到端测试。这有助于保持测试速度并降低维护成本。 "测试优先级与分类"模式强调根据业务价值和风险来确定测试的...

    测试驱动开发及代码重构

    1. **红**:首先,编写一个失败的单元测试,即一个测试用例,这个用例对应于要实现的功能。测试通常会因为缺少相应的功能代码而失败。 2. **绿**:然后,编写最小的代码量,使测试通过。这通常是一个简单的实现,只...

    XUNIT测试模式--测试码重构(英文版)

    - **目的**:确保每个测试之间没有残留的状态,每个测试都处于一致的初始条件下运行。 通过以上详细的知识点解析可以看出,《XUNIT测试模式--测试码重构》这本书不仅介绍了xUnit框架的基础概念和使用方法,更重要的是...

    Geliang_单元测试模式与重构v1.

    单元测试作为软件开发过程中的一个重要组成部分,能够帮助开发者确保代码的质量,并在重构过程中保持系统的稳定性。本文档基于葛亮2013年7月4日的分享内容进行深入探讨,旨在通过解析单元测试的基本概念、目的及实践...

    重构(Refactoring)英文版

    - **引入参数对象(Introduce Parameter Object)**:当一个方法接受多个参数时,可以创建一个新的类来封装这些参数,提高代码的可读性和可维护性。 #### 五、重构工具 随着技术的发展,现在有许多工具可以帮助...

    软件重构讲义,自己整理的一个软件重构ppt

    《软件重构讲义》这份资料,为开发者提供了一个深入理解重构概念和技术的框架。它不仅仅是对理论的简单阐述,更是通过具体实例和技术手段,向开发者展示如何在实际开发过程中,运用重构技术来改善软件设计、提高代码...

    react-旨在重构一个react事例

    综上所述,"react-旨在重构一个react事例"的项目涵盖了React组件化开发、Flux架构的应用以及代码重构的实践。通过对这个项目的学习,你可以深入理解React和状态管理,并了解如何通过重构提升代码质量和应用程序的...

    (Vector Cast)UT 工程的总结问题

    UT(Unit Testing)工程是软件开发过程中的一个重要阶段,它涉及到对软件代码的最小可测试单元进行独立验证,以确保每个功能模块的正确性。这篇总结将深入探讨在Vector Cast环境下进行UT工程时可能遇到的问题以及...

    重构_重构_改善既有代码_

    2. 内联函数(Inline Method):如果一个函数只在一个地方被调用,可以将其内容直接替换到调用位置,减少层次,提高效率。 3. 将类的职责分离(Split Class):如果一个类承担了过多职责,应将其拆分为多个更专注的...

    重构?测试?TDD?Ant?

    4. **Ant**:Ant是Apache软件基金会的一个项目,是一个基于Java的构建工具。它使用XML来定义构建过程,包括编译、打包、测试和部署等任务。与传统的Makefile相比,Ant更具有平台无关性,易于理解和使用,尤其在大型...

    《重构——改善既有代码的设计》第一个案例代码

    在这个案例中,我们聚焦于一个影片出租点的程序,通过逐步的重构过程,来演示如何优化原始代码。 重构是一种在不改变代码外在行为的前提下,改进其结构的过程。这个过程通常伴随着单元测试,确保每次修改都不会破坏...

    重构.pdf_电子版_pdf版

    重构的第一步是构建一个可靠的测试环境,这是安全重构的保障。测试环境可以确保每次代码修改之后,软件的行为和功能都能得到验证,从而保障重构的安全性。 重构的下一步是开始逐步修改程序。在这个案例中,开发者将...

    27丨理论一:什么情况下要重构?到底重构什么?又该如何重构?1

    重构是软件开发过程中的一个重要环节,它涉及到代码的优化、设计改进和质量提升,而不改变程序的外部行为。本文将详细探讨重构的目的、对象、时机和方法,帮助开发者理解和掌握重构的核心理念。 **重构的目的(Why...

    【EMD重构】.rar_EMD重构函数_IMF变量重构_tomorrowi4n_模态分解_重构

    在描述中提到的"对经验模态分解后的各分量IMF进行重构代码,函数可直接调用",意味着这个压缩包中包含了一个名为"EMDchonggou.m"的MATLAB脚本文件,该文件提供了实现IMF重构功能的代码。用户可以直接运行这个函数,...

    代码重构&模式

    代码重构是软件开发过程中的一个重要环节,它是指在不改变代码外在行为的前提下,对代码结构、设计和实现进行改进,以提高代码的可读性、可维护性和内部结构。重构的主要目标是使软件更容易理解和修改,而不仅仅是...

Global site tag (gtag.js) - Google Analytics