软件测试(五):实施单元测试技术 [转贴 2005-06-27 16:52:10 ] 发表者: yonnie   

  本文作者通过实例介绍了单元测试自动化实现的原理和方法,更难得的是,作者结合自身工作经验提出了单元测试工程实施的要领和注意事项。

  单元测试(Unit Testing)是针对于软件基本组成单元所进行的一种测试。按照《详细设计规格说明》中对软件单元的划分,单元测试人员应逐一检查软件单元的程序编码是否和设计要求完全一致。这里用到了“软件单元”这个词汇。一般地,“软件单元”是指在《详细设计规格说明》中所划分出的基本软件单元,即根据概要设计规格说明中的模块,细化出来的类、数据结构、过程或函数等。但在实际的单元测试过程中,有时为了进一步查找问题产生的根源,还会对软件单元继续细化,具体到某一个函数或方法体,或者函数、方法体内的某几段关键代码。

  实现单元测试的自动化

  目前,单元测试一般采用基于XUnit测试框架的自动化测试工具实现。如Java编程中使用的JUnit,.Net程序编程中使用的NUnit。也有一些其他的用于单元测试的工具,如Cantata或AdaTest,前者是针对于C/C++的测试工具,后者是针对于Ada语言的测试工具。单元测试工具的基本工作原理如图所示。

  从图中可以看出,自动化单元测试工具的工作原理是借助于驱动模块与桩模块,运行被测软件单元以检查输入的测试用例是否按软件详细设计规格说明的规定执行相关操作。

  这里必须先对桩模块(Stub Module)、驱动模块(Drive Module)等概念做一个简单的介绍。我们知道,软件单元在完成编码以后,代码本身并不是一个可以独立运行的程序。所以,必须为每个软件单元开发用于测试目的驱动模块和桩模块。在绝大多数应用中,驱动模块只是一个接收测试数据,并把数据传送给要测试的软件单元,然后打印或告知测试者相关测试结果的“主程序”;而桩模块的功能则是代替那些隶属于被测软件单元或与被测单元有接口关系的软件模块。这样,测试用例从驱动模块读入到被测软件单元中,被测软件单元针对给定的测试用例运行,当需要通过接口与其他模块进行通信时,就调用桩模块。被测软件单元执行完测试用例以后,将执行结果汇报给驱动模块,驱动模块再将执行结果打印出来或以其他方式(如E-mail)报告给单元测试者。

  可以通过如下一个简单的Java程序来说明单元测试的原理。这个程序由三个代码文件组成。它们分别是CaseCheck.java、Account.java以及MoneyTran.java。其中CaseCheck.java充当驱动模块,Accout.java是被测软件单元,MoneyTran.java充当桩模块。以下列出它们各自的源代码:

/* Module name: CaseCheck.java this module servers as driven module; */

public class CaseCheck{
  public static void main(String[] args){
    Account TomAccount=new Account(8000); 
    if(8000!=TomAccount.checkBalance()){ 
      System.out.println("TomAccount Construction error!");
    } 
    System.out.println("Total balance of TomAccount is "+TomAccount.checkBalance() +"\nWithdraw 1000 from TomAccount\n"+TomAccount.withdraw(1000)); 
    System.out.println("Now,total balance of TomAccount is "+TomAccount.checkBalance() +"\nWithdraw 8000 from TomAccount\n"+TomAccount.withdraw(8000)); 
    System.out.println("Desopit 2000 to Tom's Account."); 
    TomAccount.deposit(2000); 
    if(9000!=TomAccount.checkBalance()){
      System.out.println("Account class deposit method error!");
    }
    System.out.println("Now Tom's Account has Rmb "+TomAccount.checkBalance()+" .It can change into USDollar "+TomAccount.toDollar());
  }
}

/* Module name: Account.java this module servers as software unit for test; */

public class Account{
  private int sum;
  public Account(int num){
    sum=num;
  }

  public String withdraw(int num){
    if(num>sum){
      return "Overdraft.Operation cancelled."+"\n";
    }else{
      sum-=num;
      return "Withdraw Success."+"\n";
    }
  }

  public void deposit(int num){
    sum+=num;
  }

  public int checkBalance(){
    return sum;
  }

  public int toDollar(){
    double rate=MoneyTran.RmbtoDollar();
    return (int) (rate*sum);
  }
}

/* Module name:MoneyTran.java this module servers as stub module; */

public class MoneyTran{
  static double RmbtoDollar(){
    return 0.12081964;
  }
}

  在上述代码中,被测单元Account.java有构造方法、存钱(deposit)、取钱(withdraw)、余款查询(checkBalance)以及人民币与美元兑换(toDollar)这些方法。CaseCheck.java构造了TomAccount这个对象,测试该对象上述方法是否能正常工作。注意到toDollar方法中调用了另一模块中的RmbtoDollar这个静态方法,因此,在本测试程序中加入了桩模块MoneyTran。实际中MoneyTran的RmbtoDollar()方法可能要完成实时的数据表查询操作,然而,因为被测单元是Account.java,所以采用一个简单的数值返回就行了。

  目前常用的JUnit以及Nunit基本上都采用了上述实现架构。例如,在JUnit中上述CaseCheck.java文件可以用如下文件代替:

import junit.framework.*;

/* * Using junit to complete test task. */

public class JunitCaseCheck extends TestCase {
  protected Account TomAccount;
  protected Account NullPointer;

  public JunitCaseCheck(String args0){
    super(args0);
  }

  protected void setUp() throws Exception{
    TomAccount=new Account(8000);
  }

  protected void tearDown() {
    TomAccount=NullPointer;
  }

  public static Test suite() {
    return new TestSuite(JunitCaseCheck.class);
  }

  public void testConstructor() {
    assertTrue(TomAccount.checkBalance()== 8000);
  }

  public void testWithdraw(){
    TomAccount.withdraw(1000);
    assertTrue(TomAccount.checkBalance()== 7000);
  }

  public void testDeposit(){
    TomAccount.deposit(1000);
    assertTrue(TomAccount.checkBalance()== 9000);
  }

  public void testtoDollar(){
    assertTrue((int)(MoneyTran.RmbtoDollar()*8000)==TomAccount.toDollar());
  }

  public static void main(String[] args){
    junit.swingui.TestRunner.run(JunitCaseCheck.class);
  }
}

  保持其他两个java文件不变,可以看到JUnit将以绿条表示上述测试全部通过。由于NUnit一般采用C#描述测试脚本,上述三个程序都要做一些词法上的调整。

  单元测试工具之外的工作

  单元测试工具必须在人的辅助下完成单元测试任务。测试人员在运行单元测试工具之前,应设计好相应的测试用例。然后将测试用例输入驱动模块进行相应软件单元的测试工作。

  如何设计高质量的测试用例是一个很有技术含量的论题,而且,一个设计完好的测试用例本身有时也应随编程语言、运行环境等做适应性修改。这里笔者给出几点经验之谈。

  ● 设计正常测试用例

  这里,正常测试用例是指在实际业务中经常使用到的、不能出错的测试用例。设计正常的测试用例,关键是做到全面。要充分考虑到系统客户可能会实际面对的各种应用的情境,而不能只测一种或几种应用情境,忽略其他的情境。

  ● 设计边界值测试用例

  边界值测试用例是指使用处于条件的边界的数值来测试被测软件单元能否作出预期反应的测试用例。比如对于一个int型数据,可以考虑输入一个int型数据最大值看会发生什么情况,又比如对于循环或条件语句,可以考虑输入条件的临界值,看看软件单元如何反应。边界值测试用例能较好地暴露编码人员逻辑不严密的地方。

  ● 设计异常测试用例

  异常测试用例非常重要。一般的开发过程只测试代码能否在正常情境中工作,而不测试代码能否针对异常情境所做出适当的反应。这种做法是很片面的,对于复杂系统而言,更要引起足够重视。设计异常测试用例采取的方法是输入一些古怪的数值,看软件单元如何反应。如输入越界数值,输入类型不匹配的数值,输入参数个数不匹配等。

  除了设计测试用例以外,单元测试人员还应该对关键软件部件的程序代码做必要的核查,检查编码人员是否在代码中引入了“后门”(一种能侵入系统取得控制权或窃取数据的程序段代码)、代码中是否存在冗余代码等。

  单元测试工程实施要领

  在工程实践中,单元测试应该坚持如下原则进行展开:

  ● 单元测试越早进行越好。在TDD方法中,Kent甚至认为开发团队应该遵行“先写测试、再写代码”的编程途径。

  ● 对于修改过的代码应该重做单元测试,以保证对已发现错误的修改没有引入新的错误。

  ● 测试人员的测试用例应经过审核,如有必要应经过会议评审,以保证测试用例的质量。

  ● 当测试用例的测试结果与设计规格说明上的预期结果不一致时,测试人员应如实记录实际的测试结果。

  除了上述四点原则之外,单元测试还应注意以下几点:

  ● 单元测试应该依据《软件详细设计规格说明》进行,而不要只看代码,不看设计文档。因为只查代码,仅仅能验证代码有没有做某件事,而不能验证它应不应该做这件事。

  ● 单元测试应注意选择好被测软件单元的大小。软件单元划分太大,那么内部逻辑和程序结构就会变得很复杂,造成测试用例过于繁多,令用例设计和评审人员疲惫不堪;而软件单元划分太细会造成测试工作太繁琐,失去效率。工程实践中要适当把握好划分原则,不能过于拘泥。

  ● 注意使用单元测试工具。目前市面上有很多可以用于单元测试的工具。如果一味地排斥自动化测试工具,有可能会导致大量的重复劳动。因此,好的测试团队应对市面的测试工具保持高度敏感,并在技术条件许可的情况下尽量开发一些通用的自主版权的测试工具。这样日积月累,测试团队就能很好地把握测试进度,降低工作强度,把测试人员的精力花在更有创造性的工作上。