专注于互联网--专注于架构

最新标签
网站地图
文章索引
Rss订阅

首页 »Java教程 » junit:JUnit 反模式 »正文

junit:JUnit 反模式

来源: 发布时间:星期四, 2009年1月8日 浏览:13次 评论:0
  JUnit 出现为开发人员带来了福音遗憾许多人仍然认为学会 JUnit API编写几个测试最后得到个测试良好应用就足够了这种想法比不进行任何测试还要糟这会导致对代码健康状态误解学习 JUnit 是测试中最容易部分编写优秀测试则是较困难个环节本文将介绍些常见 JUnit 反模式并介绍说明如何解决它们

  两个月前我和妻子决定在厨房里装上木镶板这是我第次装修房子我带着股盲目乐观主义精神使用铁锤和钉子干起了装修但这样做几乎是场灾难我用不好铁锤最后妻子不得不重新修整被我敲打得高低不平镶板和出现裂缝

  在装修卧室时我认为已学到了些经验教训这次借来了岳父气钉枪仅用了装修厨房十分的时间就装修完了卧室但是气钉枪不能弥补我在其他方面失误 —— 例如忘记了保持木板顶部水平切割木板时切错了位置忘记检查木板将有裂纹木板钉了上去等等还出现了其他许多问题这些问题幸好都被细心妻子注意到了通过此事我认识到:气钉枪不如个木匠

  JUnit:气钉枪式测试工具

  我认为JUnit 很像爸爸气钉枪JUnit 出现的前测试不是不可能但是非常困难事实上它困难到了致使通常没有人愿意进行测试即使进行测试也仅仅是对那些看起来特别复杂或脆弱以致人们有理由进行额外测试那部分

  JUnit 就是专门解决此问题工具这里不可告人秘密是此现象致使许多编程人员实际上乐于 编写些测试这样就造成了编程人员编写测试而客户期盼测试情形尽管仍有些坚持者但多数客户现在开始倾向于使用我们在测试领域新霸主 JUnit(有关热爱测试更多信息请参阅 参考资料)

  问题是JUnit 不是万能药它是种名副其实工具像其他优秀工具样(JUnit 是最优秀工具的)JUnit 只做件事情并且能出色地完成它提供个用于执行测试框架具体表现在:

  JUnit 提供个用于编写测试模板该模板可以安装、执行和卸载

  它允许您在层次结构中组织测试

  它允许您自动而又方便地执行测试

  它减少了来自执行过程中测试报告量允许使用同测试套件中区别测试操作

  尽管 JUnit 功能强大使用起来很简单但是它也存在许多不足的处需要其他工具来填补这些缺陷以下是 JUnit 无法做到:

  对被测试单元自动生成测试

  提供覆盖条件

  编写了劣质测试时进行提示

  阐明观点

  Robert Binder 编写了本好书名称为 Testing Object-Oriented s: Models, Patterns, and ToolsBinder 是位少有天才人物 —— 个测试圣人作为本测试方面参考资料该书价值是无法衡量Binder 在本书开头再次谈及 Scott Meyers 测试问题这个问题就是为 Triangle 对象编写单元测试

  Java™ 技术实现了个采用 3个边长构造边各有个 getters 和 ters该技术实现有 3种思路方法:isIsosceles、isScalene 和 isEquilateral其中每种思路方法都可以返回 true 或 false具体情况取决于 3角形配置triangle 还是 Polygon 类型个子类, 后者由 Figure 类派生而来Figure 是代表对象抽象类该类可以通过光栅显示描绘现在面临挑战是如何编写此类测试

  Binder 从 Meyers 原始解决方案中列出了 33 个测试并提供了 32 个和面向对象问题属性有密切关系测试所以现在共有 65 个测试除非是影响生命安全重要软件Software否则您可能从来不会如此详细地测试代码也不会了解到原来它是如此测试原因不是您有生理缺陷或者懒惰而是您没有受过测试方面训练您将专用开发时间都消耗在了编程窍门技巧上而不是消耗在测试能力上该如何办呢?JUnit 可让测试变得简单易行

  反模式

  本部分将介绍几个反模式其中现象是我们经常遇到或易犯

  愉快路径测试

  愉快路径测试 可以验证被测系统行为是否为所期望行为它们遵循每个正确执行路径在功能测试中愉快路径和实际用例相同或相近在单元测试中它和实际用例相同或更小单元服从于“单职责原则”您是测试它职责

  实际上愉快路径测试并不是个反模式反模式是指在进行愉快路径测试时开发停止行为愉快路径不测试系统部分(不愉快路径)编写代码时通常考虑使用愉快路径进行编写甚至在头脑中用些愉快路径数据对它进行测试边界条件将等待未测试、范围的外数据允许它们将您应用带到其管辖范围的内

  假设您正在编写个包含思路方法 eval Factorial 类该思路方法携带 并返回该 阶乘个愉快路径测试会确认 Factorial.eval(3) 返回是 6此代码实现不正确但它仍返回正确结果(误报)这种几率非常小:

public Factorial {
  public eval( _num) {
     (_num 1) { 1; }
     _num * eval(_num - 1);
  }
}


  有些人会对此测试感到满意并继续操作但是请考虑下面这个实现:

public Factorial {
  public eval( _num) {
     6;
  }
}


  出现误报(false positive)会怎样呢?如果您从未接触过由测试驱动开发(请参阅 参考资料)那么您可能也会认为人人都能编写如此头脑简单实现测试驱动开发 (TDD) 中个练习就是首先编写测试然后执行可能运行最简单操作 —— 如本例中 6

  即使没有使用 TDD 思路方法执行操作并在正确实现中出现您仍会得到误报请考虑以下实现:

public Factorial {
  public eval( _num) {
     (_num 1) { 1; }
     _num + eval(_num - 1);
  }
}


  除了数字序列是相加而不是相乘的外这个算法和第个算法几乎是相同对于值 3 和值 1(恰好出现这样个值)返回值是但是对于其他任何值则会失败关键是碰巧通过个测试并不困难

  这就是为什么定要进行两次以上愉快路径测试测试两次可以明显地减少致通过机率尤其是测试值是 orthogonal (相互独立或没有关系)情况下例如编写个值为 3 和 5 测试将很就可以看出前面两个实现是

  确认测试和边界测试

  还需要考虑其他两个测试类型:validity(或 do)和 boundary前者声明无效数据(或域外数据)正确行为后者是愉快路径测试种形式但它声明实现在域边界上可以正确地运行

  在这个举例中请考虑在 Factorial.eval(-3) 时将会发生什么情况很有可能用尽堆栈空间造成崩溃当然 -3 不是个有效输入所以使用它毫无意义但是在正确和的间还有个中间思路方法称为 IllegalArgumentException演示如下:

public Factorial {
  public eval( _num) {
     (_num < 1) {
      throw IllegalArgumentException(
        "Parameter must be greater than 0: " + _num);
      }
     (_num 1) { 1; }
     _num * eval(_num - 1);
  }
}


  编写了阶乘代码后您可能发现该代码仍有所以让我们谈下边界测试如果存在个边界那么输入参数为 0这是个有效输入从数学上说0 阶乘是 1执行前面实现会导致测试失败您希望返回值是 1但得到却是 IllegalArgumentException还应该检查边界边 -1以验证可以得到期望 IllegalArgumentException而不是个整数

  对其他边界相应测试将留做练习供您操练提示:如果执行 Factorial.eval(100) 将会发生什么情况?

  简单测试

  和愉快路径反模式简单测试反模式讲不是有关“是什么”而是“不是 什么”若开发人员没有经验并且代码难以测试则通常会出现这种症状结果您会看到对容易测试 (equals 和 toString 往往很突出参见清单 1) 内容进行多次测试而被测单元真正逻辑却被忽略了结果出现了许多不能检测系统传递测试这会导致对代码健康状态误解

  清单 1. 些容易测试签名

testEqualsReflexive
testEqualsSymmetric
testEqualsTransitive
testEqualsOnNullParameter
testEqualsWorksMoreThanOnce
testEqualsFailsOnSub
testEqualsIsStillReflexive


  进行系统测试的所以困难您经常尝试测试某个思路方法而不是检测某个装置假设您要测试个堆栈实现那么您测试签名可能如清单 2 所示

  清单 2. 用于堆栈单元测试可能测试签名

testPopHappyPath;
testPopEmptyStack;
testPushHappyPath;
testPushFullStack;
testPeek;


  其中有些测试很容易如清单 3 所示

  清单 3. 用于空堆栈单元测试

public void testPopEmptyStack {
  Stack stackUT = Stack;
  assertEquals(0, stackUT.getSize);
  try {
    stackUT.pop;
    fail("Expected StackUnderflowException");
  } catch (StackUnderflowException _expected) {}
}


  但是如何测试 push 愉快路径呢?

  清单 4. 用于 stack.push 元单测试

public void testPushHappyPath {
  Stack stackUT = Stack;
  Object item = Object;
  stackUT.push(item);
  // now what?
}


  这是测试单元实现常见而不是单元和其客户机签定契约假设 push 思路方法实现方式如下:

public Stack {
  private List elements;
  ...
  public void push(Object _element) {
    elements.add(_element);
  }
}


  您需要进行这测试来验证 elements List 现在是否含有 push 添加 Object所以您要编写如下测试:

public void testPushHappyPath {
  Stack stackUT = Stack;
  Object expectedElement = Object;
  stackUT.push(expectedElement);
  List elements = stackUT.getElementsList;
  assertEquals(1, elements.size);
  assertEquals(expectedElement, elements.get(0));
}


  其中问题是破坏了封装原因是公开了被测单元内幕相反要测试 push 是否将对象放入了列表您应测试堆栈和客户机签定契约J.B. Rainsberger 将此称为测试装置 (fixture)

  现在测试如清单 5 所示

  清单 5. 用于堆栈装置单元测试

public void testPushPop {
  Stack stackUT = Stack;
  Object expectedElement = Object;
  assertEquals(expectedElement, stackUT.push(expectedElement).pop;
  assertTrue(stackUT.isEmpty);
}
public void testFILO {
  Stack stackUT = Stack;
  Object expectedOne = Object;
  Object expectedTwo = Object;
  stackUT.push(expectedOne);
  stackUT.push(expectedTwo);
  assertEquals(expectedTwo, stackUT.pop);
  assertEquals(expectedOne, stackUT.pop);
  assertTrue(stackUT.isEmpty);
}


  您将不会再破坏封装原因是您没有声明单元在封装中如何运行相反您充分利用了该装置显示严密内聚性拥有可以推动但不能弹出堆栈没有任何意义因此您可以将这些思路方法作为堆栈暴露给其客户机契约部分进行测试

  当编写代码时应考虑到这个契约 —— 您将要编写特定内容都将暴露给它客户机无论此内容是个思路方法、个类还是个和类交互该契约是您要测试个内容而不是实现细节以这种形式进行测试将有助于该契约形式化使该契约更为明确并能够通过测试得到很好定义而不会处于不确定和非正式状态

  过度复杂测试

  当测试明显正确时该测试通常会成功如果测试很复杂以致于不能立即断定它是否正确那么您将无法知道该测试是否测试(甚至更糟是不知道它是否正被地传递)而导致失败当被测系统需要个复杂设置或暴露需要拆分复杂数据结构时通常会出现这种情况

  请考虑这样个例子在这个例子中有个代码该代码携带些客户数据并将其写出保存到个有固定记录文件中以便在旧式系统中使用您大概不会对记录是否为正确格式测试感兴趣 —— 在这些方面您已经进行了许多测试您要测试是记录中是否存在正确数据在这种情况下很容易看到如清单 6 所示测试

  清单 6. 过度复杂测试

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import junit.framework.TestCase;
public RecordTest extends TestCase {
  public void testRecordContainsCorrectCustomerData {
    // up
    String expectedName = "Estragon";
     expectedId = 1001;
    String expectedItemNames = {"A man", "A plan", "A canal", "Suez"};
    Customer customer = Customer(expectedId, expectedName, expectedItemNames);
    // execute
    BillingCenter.processCustomer(customer);
    // assert results
    File file = File("customer.rec");
    assertTrue(file.exists);
    FileInputStream fis = FileInputStream(file);
    ByteArrayOutputStream baos = ByteArrayOutputStream;
     buffer = [16];
     numRead;
    while ((numRead = fis.read(buffer)) >= 0) {
      baos.write(buffer, 0, numRead);
    }
     record = baos.toByteArray;
    assertEquals(128, record.length); // exactly _disibledevent= 0; i < 4; i) {
      assertEquals(expectedItemNames[i], record.getItemName(i));
    }
  }
}


  现在测试代码清楚地表示出了该测试意图毫无疑问此测试是正确它已经完成了设置预期值被测试系统 getter 和作出声明该逻辑被应用到了 RecordFileFacade 和 RecordFacade 类中RecordFileFacade 负责从文件中读取数据并成批将它们送入记录中RecordFacade 负责解析每条记录并通过 Java 语言友好测试思路方法公开这些这些数据这个测试个优点是 RecordFileFacade 和 RecordFacade 现在也能够测试当拆分记录逻辑保存在该测试中时将无法对其进行测试

  最好将该逻辑应用到基础结构中个优秀测试应当满足以下条件:

  设置

  声明预期结果

  练习被测试单元

  获得实际结果

  声明实际结果是否和预期结果相符

  个测试良好应用不仅仅包含应用代码和测试定数量基础结构代码可以充当测试和被测系统的间适配器此用途有两个:其可以允许测试清楚地表示其意图其 2通过将复杂代码抽象到独立层中还能够为该层编写测试

  结束语

  在许多思路方法中使用 JUnit 进行测试更方便测试编写代码越来越趋向于进行坏测试和好测试但是1,000 个坏传递测试比不进行试测更糟糕测试会给您自信意识

  编写测试时定要注意所编写测试质量:

  不要仅测试愉快路径还要测试边界条件和范围的外

  不要测试实现而是要测试装置

  不要使您测试代码比被测代码更复杂

  总的要通过不懈努力来扩展您测试窍门技巧使的成为专业开发部分在测试工作方面不要将全部精力都用在编程窍门技巧上



0

相关文章

读者评论

发表评论

  • 昵称:
  • 内容: