軟件測試(五):實施單元測試技術 [轉貼 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甚至認為開發團隊應該遵行“先寫測試、再寫代碼”的編程途徑。

  ● 對于修改過的代碼應該重做單元測試,以保證對已發現錯誤的修改沒有引入新的錯誤。

  ● 測試人員的測試用例應經過審核,如有必要應經過會議評審,以保證測試用例的質量。

  ● 當測試用例的測試結果與設計規格說明上的預期結果不一致時,測試人員應如實記錄實際的測試結果。

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

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

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

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