軟件測試(五):實施單元測試技術(shù) [轉(zhuǎn)貼 2005-06-27 16:52:10 ] 發(fā)表者: yonnie   

  本文作者通過實例介紹了單元測試自動化實現(xiàn)的原理和方法,更難得的是,作者結(jié)合自身工作經(jīng)驗提出了單元測試工程實施的要領(lǐng)和注意事項。

  單元測試(Unit Testing)是針對于軟件基本組成單元所進行的一種測試。按照《詳細設(shè)計規(guī)格說明》中對軟件單元的劃分,單元測試人員應(yīng)逐一檢查軟件單元的程序編碼是否和設(shè)計要求完全一致。這里用到了“軟件單元”這個詞匯。一般地,“軟件單元”是指在《詳細設(shè)計規(guī)格說明》中所劃分出的基本軟件單元,即根據(jù)概要設(shè)計規(guī)格說明中的模塊,細化出來的類、數(shù)據(jù)結(jié)構(gòu)、過程或函數(shù)等。但在實際的單元測試過程中,有時為了進一步查找問題產(chǎn)生的根源,還會對軟件單元繼續(xù)細化,具體到某一個函數(shù)或方法體,或者函數(shù)、方法體內(nèi)的某幾段關(guān)鍵代碼。

  實現(xiàn)單元測試的自動化

  目前,單元測試一般采用基于XUnit測試框架的自動化測試工具實現(xiàn)。如Java編程中使用的JUnit,.Net程序編程中使用的NUnit。也有一些其他的用于單元測試的工具,如Cantata或AdaTest,前者是針對于C/C++的測試工具,后者是針對于Ada語言的測試工具。單元測試工具的基本工作原理如圖所示。

  從圖中可以看出,自動化單元測試工具的工作原理是借助于驅(qū)動模塊與樁模塊,運行被測軟件單元以檢查輸入的測試用例是否按軟件詳細設(shè)計規(guī)格說明的規(guī)定執(zhí)行相關(guān)操作。

  這里必須先對樁模塊(Stub Module)、驅(qū)動模塊(Drive Module)等概念做一個簡單的介紹。我們知道,軟件單元在完成編碼以后,代碼本身并不是一個可以獨立運行的程序。所以,必須為每個軟件單元開發(fā)用于測試目的驅(qū)動模塊和樁模塊。在絕大多數(shù)應(yīng)用中,驅(qū)動模塊只是一個接收測試數(shù)據(jù),并把數(shù)據(jù)傳送給要測試的軟件單元,然后打印或告知測試者相關(guān)測試結(jié)果的“主程序”;而樁模塊的功能則是代替那些隸屬于被測軟件單元或與被測單元有接口關(guān)系的軟件模塊。這樣,測試用例從驅(qū)動模塊讀入到被測軟件單元中,被測軟件單元針對給定的測試用例運行,當(dāng)需要通過接口與其他模塊進行通信時,就調(diào)用樁模塊。被測軟件單元執(zhí)行完測試用例以后,將執(zhí)行結(jié)果匯報給驅(qū)動模塊,驅(qū)動模塊再將執(zhí)行結(jié)果打印出來或以其他方式(如E-mail)報告給單元測試者。

  可以通過如下一個簡單的Java程序來說明單元測試的原理。這個程序由三個代碼文件組成。它們分別是CaseCheck.java、Account.java以及MoneyTran.java。其中CaseCheck.java充當(dāng)驅(qū)動模塊,Accout.java是被測軟件單元,MoneyTran.java充當(dāng)樁模塊。以下列出它們各自的源代碼:

/* 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有構(gòu)造方法、存錢(deposit)、取錢(withdraw)、余款查詢(checkBalance)以及人民幣與美元兌換(toDollar)這些方法。CaseCheck.java構(gòu)造了TomAccount這個對象,測試該對象上述方法是否能正常工作。注意到toDollar方法中調(diào)用了另一模塊中的RmbtoDollar這個靜態(tài)方法,因此,在本測試程序中加入了樁模塊MoneyTran。實際中MoneyTran的RmbtoDollar()方法可能要完成實時的數(shù)據(jù)表查詢操作,然而,因為被測單元是Account.java,所以采用一個簡單的數(shù)值返回就行了。

  目前常用的JUnit以及Nunit基本上都采用了上述實現(xiàn)架構(gòu)。例如,在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#描述測試腳本,上述三個程序都要做一些詞法上的調(diào)整。

  單元測試工具之外的工作

  單元測試工具必須在人的輔助下完成單元測試任務(wù)。測試人員在運行單元測試工具之前,應(yīng)設(shè)計好相應(yīng)的測試用例。然后將測試用例輸入驅(qū)動模塊進行相應(yīng)軟件單元的測試工作。

  如何設(shè)計高質(zhì)量的測試用例是一個很有技術(shù)含量的論題,而且,一個設(shè)計完好的測試用例本身有時也應(yīng)隨編程語言、運行環(huán)境等做適應(yīng)性修改。這里筆者給出幾點經(jīng)驗之談。

  ● 設(shè)計正常測試用例

  這里,正常測試用例是指在實際業(yè)務(wù)中經(jīng)常使用到的、不能出錯的測試用例。設(shè)計正常的測試用例,關(guān)鍵是做到全面。要充分考慮到系統(tǒng)客戶可能會實際面對的各種應(yīng)用的情境,而不能只測一種或幾種應(yīng)用情境,忽略其他的情境。

  ● 設(shè)計邊界值測試用例

  邊界值測試用例是指使用處于條件的邊界的數(shù)值來測試被測軟件單元能否作出預(yù)期反應(yīng)的測試用例。比如對于一個int型數(shù)據(jù),可以考慮輸入一個int型數(shù)據(jù)最大值看會發(fā)生什么情況,又比如對于循環(huán)或條件語句,可以考慮輸入條件的臨界值,看看軟件單元如何反應(yīng)。邊界值測試用例能較好地暴露編碼人員邏輯不嚴密的地方。

  ● 設(shè)計異常測試用例

  異常測試用例非常重要。一般的開發(fā)過程只測試代碼能否在正常情境中工作,而不測試代碼能否針對異常情境所做出適當(dāng)?shù)姆磻?yīng)。這種做法是很片面的,對于復(fù)雜系統(tǒng)而言,更要引起足夠重視。設(shè)計異常測試用例采取的方法是輸入一些古怪的數(shù)值,看軟件單元如何反應(yīng)。如輸入越界數(shù)值,輸入類型不匹配的數(shù)值,輸入?yún)?shù)個數(shù)不匹配等。

  除了設(shè)計測試用例以外,單元測試人員還應(yīng)該對關(guān)鍵軟件部件的程序代碼做必要的核查,檢查編碼人員是否在代碼中引入了“后門”(一種能侵入系統(tǒng)取得控制權(quán)或竊取數(shù)據(jù)的程序段代碼)、代碼中是否存在冗余代碼等。

  單元測試工程實施要領(lǐng)

  在工程實踐中,單元測試應(yīng)該堅持如下原則進行展開:

  ● 單元測試越早進行越好。在TDD方法中,Kent甚至認為開發(fā)團隊?wèi)?yīng)該遵行“先寫測試、再寫代碼”的編程途徑。

  ● 對于修改過的代碼應(yīng)該重做單元測試,以保證對已發(fā)現(xiàn)錯誤的修改沒有引入新的錯誤。

  ● 測試人員的測試用例應(yīng)經(jīng)過審核,如有必要應(yīng)經(jīng)過會議評審,以保證測試用例的質(zhì)量。

  ● 當(dāng)測試用例的測試結(jié)果與設(shè)計規(guī)格說明上的預(yù)期結(jié)果不一致時,測試人員應(yīng)如實記錄實際的測試結(jié)果。

  除了上述四點原則之外,單元測試還應(yīng)注意以下幾點:

  ● 單元測試應(yīng)該依據(jù)《軟件詳細設(shè)計規(guī)格說明》進行,而不要只看代碼,不看設(shè)計文檔。因為只查代碼,僅僅能驗證代碼有沒有做某件事,而不能驗證它應(yīng)不應(yīng)該做這件事。

  ● 單元測試應(yīng)注意選擇好被測軟件單元的大小。軟件單元劃分太大,那么內(nèi)部邏輯和程序結(jié)構(gòu)就會變得很復(fù)雜,造成測試用例過于繁多,令用例設(shè)計和評審人員疲憊不堪;而軟件單元劃分太細會造成測試工作太繁瑣,失去效率。工程實踐中要適當(dāng)把握好劃分原則,不能過于拘泥。

  ● 注意使用單元測試工具。目前市面上有很多可以用于單元測試的工具。如果一味地排斥自動化測試工具,有可能會導(dǎo)致大量的重復(fù)勞動。因此,好的測試團隊?wèi)?yīng)對市面的測試工具保持高度敏感,并在技術(shù)條件許可的情況下盡量開發(fā)一些通用的自主版權(quán)的測試工具。這樣日積月累,測試團隊就能很好地把握測試進度,降低工作強度,把測試人員的精力花在更有創(chuàng)造性的工作上。