單元測試

在電腦編程中，單元測試（Unit Testing）又稱為模塊測試，是針對程序模塊（軟體設計的最小單位）來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中，一個單元就是單個程序、函數、過程等；對於面向對象編程，最小單元就是方法，包括基類（超類）、抽象類、或者派生類（子類）中的方法。

通常來說，程式設計師每修改一次程式就會進行最少一次單元測試，在編寫程式的過程中前後很可能要進行多次單元測試，以證實程式達到軟體規格書要求的工作目標，沒有程序錯誤；雖然單元測試不是必須的，但也不壞，這牽涉到專案管理的政策決定。

每個理想的測試案例獨立於其它案例；為測試時隔離模塊，經常使用stubs、mock或fake等測試馬甲程序。單元測試通常由軟體開發人員編寫，用於確保他們所寫的代碼符合軟體需求和遵循開發目標。它的實施方式可以是非常手動的（透過紙筆），或者是做成構建自動化的一部分。

## 常見方式

單元測試常常通過斷言實現，開發者定義好某個模塊或函數在特定輸入下的預期輸出，然後在測試中，實際執行這個模塊或函數，檢查實際輸出和預期輸出是否匹配，如果匹配，則可以基本證明該模塊或函數在當前條件下工作正常，但需要多個單元測試配合，確保覆蓋模塊或函數的絕大多數邏輯路徑，才能基本確認該模塊或函數完全正常。

## 收益

單元測試的目標是隔離程序部件並證明這些單個部件是正確的。一個單元測試提供了代碼片斷需要滿足的嚴密的書面規約。因此，單元測試帶來了一些益處。

單元測試在軟體開發過程的早期就能發現問題。

### 適應變更

單元測試允許程式設計師在未來重構代碼，並且確保模塊依然工作正確（複合測試）。這個過程就是為所有函數和方法編寫單元測試，一旦變更導致錯誤發生，藉助於單元測試可以快速定位並修復錯誤。

可讀性強的單元測試可以使程式設計師方便地檢查代碼片斷是否依然正常工作。良好設計的單元測試案例覆蓋程序單元分支和循環條件的所有路徑。

在連續的單元測試環境，通過其固有的持續維護工作，單元測試可以延續用於準確反映當任何變更發生時可執行程序和代碼的表現。藉助於上述開發實踐和單元測試的覆蓋，可以分分秒秒維持準確性。

### 簡化集成

單元測試消除程序單元的不可靠，採用自底向上（bottom-up）的測試路徑。通過先測試程序部件再測試部件組裝，使集成測試變得更加簡單。

業界對於人工集成測試的必要性存在較大爭議。儘管精心設計的單元測試體系看上去實現了集成測試，因為集成測試需要人為評估一些人為因素才能證實的方面，單元測試替代集成測試不可信。一些人認為在足夠的自動化測試系統的條件下，人力集成測試組不再是必需的。事實上，真實的需求最終取決於開發產品的特點和使用目標。另外，人工或手動測試很大程度上依賴於組織的可用資源。

### 文檔記錄

單元測試提供了系統的一種文檔記錄。藉助於查看單元測試提供的功能和單元測試中如何使用程序單元，開發人員可以直觀的理解程序單元的基礎API。

單元測試具體表現了程序單元成功的關鍵特點。這些特點可以指出正確使用和非正確使用程序單元，也能指出需要捕獲的程序單元的負面表現（譯註：異常和錯誤）。儘管很多軟體開發環境不僅依賴於代碼做為產品文檔，在單元測試中和單元測試本身確實文檔化了程序單元的上述關鍵特點。

另一方面，傳統文檔易受程序本身實現的影響，並且時效性難以保證（如設計變更、功能擴展等在不太嚴格時經常不能保持文檔同步更新）。

### 表達設計

在測試驅動開發的軟體實踐中，單元測試可以取代正式的設計。每一個單元測試案例均可以視為一項類、方法和待觀察行為等設計元素。下面的Java例可以幫助說明這一點。

這是一個證明一批實現設計元素的測試類。首先，要求有一個名為Adder的接口，和一個不帶參數的構造方法名為AdderImpl的實現類。然後，它斷言Adder接口包含有一個兩個整數參數返回值為整型的add方法。它也通過小範圍的值檢驗說明方法的行為。

```java
public class TestAdder {
    public void testSum() {
        Adder adder = new AdderImpl();
        assert(adder.add(1, 1) == 2);
        assert(adder.add(1, 2) == 3);
        assert(adder.add(2, 2) == 4);
        assert(adder.add(0, 0) == 0);
        assert(adder.add(-1, -2) == -3);
        assert(adder.add(-1, 1) == 0);
        assert(adder.add(1234, 988) == 2222);
    }
}
```

這個案例中，單元測試在程序之前寫成，用作指明待設計的對象形態和行為的文檔，沒有任何實現細節，留作程式設計師練習。以下可能是最簡單的工作實踐，這個最容易的解決方案可以通過上述測試：

```java
interface Adder {
    int add(int a, int b);
}
class AdderImpl implements Adder {
    int add(int a, int b) {
        return a + b;
    }
}
```

不同於其他基於圖的設計方法，用單元測試表達設計有一項顯著優點：設計文檔（單元測試本身）可以用於驗證程序實現符合設計。UML可能會遇到這樣的問題：儘管圖上一個類被命名為Customer，但開發人員可以稱其為Wibble，而且系統中沒有任何地方會顯示出這個差異。基於單元測試設計方法，開發人員不遵循設計要求的解決方案永遠不會通過測試。

當然，單元測試缺乏圖的可讀性，但UML圖可以在自由工具（通常可從IDE擴展獲取）中為大多數現代程序語言生成UML圖，很難要求採購昂貴的UML設計套裝軟體。自由工具，類似於基於xUnit框架的工具，測試結果輸出到一些可生成供人工識讀的圖形化工具系統中去。

## 分離接口和實現

因為很多類會引用其它類，對這個類的測試經常會要求測試其它的類。一個最普遍的例子是依賴於資料庫的類：為了測試它，測試人員通常編寫代碼去操作資料庫。這是不對的，因為單元測試不應超出待測試的類邊界。

作為替代，軟體開發人員應創建一個資料庫連接的抽象接口，然後實現這個接口的模擬對象。通過對代碼所需附件的抽象（臨時降低了網狀的耦合效應），這些獨立程序單元較前者更能被完整測試。高質量的代碼單元也可提供更好的可維護性。

## 局限

測試不可能發現所有的程序錯誤，單元測試也不例外。按定義，單元測試只測試程序單元自身的功能。因此，它不能發現集成錯誤、性能問題、或者其他系統級別的問題。單元測試結合其他軟體測試活動更為有效。與其它形式的軟體測試類似，單元測試只能表明測到的問題，不能表明不存在未測試到的錯誤。

軟體測試是一個組合問題。例如，每一個布爾型的決斷語句需要至少兩種測試：一個返回真，一個返回假。因此，針對每行書寫的代碼，程式設計師通常需要寫3至5行的測試代碼。這很明顯地很花時間而且對此的投入可能並不值得。也有些問題是根本不能簡單地檢測出來的——例如具不確定性的或牽扯到多線程的問題。此外，替單元測試寫的程式碼可能就像要測試的程式碼一樣有程序錯誤。佛瑞德·布魯克斯在人月神話一書中舉例說明：「絕對不要帶兩個計時器去海邊。最好總是帶一或三個」。意味著，如果兩個計時器互相衝突的話，你該怎麼知道哪個是對的？為了獲得單元測試的好處，在軟體開發過程中應形成一套嚴格紀律意識。仔細保留記錄是必要的，不僅僅只保留執行的測試，也包括保留對應的源碼和其它軟體單元的變更歷史。即，使用版本控制系統是必要的。如果後續版本不能通過一個以前測試通過的單元測試，版本控制系統可以提供對應時間段對原始碼所做的變更清單。

每天養成查看單元測試案例失敗測試並及時確定錯誤原因的習慣是必要的。如果沒有這樣的流程，沒有在團隊工作流程中體現，單元測試系列將走向不同步，造成越來越多的錯誤和越來越低效的單元測試案例系列。

## 應用

### 極限編程

單元測試是極限編程的基礎，依賴於自動化的單元測試框架。自動化的單元測試框架可以來源於第三方，如xUnit，也可以由開發組自己創建。

極限編程創建單元測試用於測試驅動開發。首先，開發人員編寫單元測試用於展示軟體需求或者軟體缺陷。因為需求尚未實現或者現有代碼中存在軟體缺陷，這些測試會失敗。然後，開發人員遵循測試要求編寫最簡單的代碼去滿足它，直到測試得以通過。

系統中大多數代碼都經過單元測試，但並非所有代碼路徑都必需單元測試。極限編程強調「測試所有可能中斷」的策略，而傳統方法是「測試所有執行路徑」。這使得極限編程開發人員比傳統開發少寫單元測試，但這並不是問題。不爭的事實是傳統方法很少完全遵循完整地測試所有執行路徑的要求。極限編程相互地認識到測試很少能完備（因為完備測試通常需要昂貴的代價和時間消耗，意味著不經濟），提供了如何有效地將有限資源集中投入可花費的代價到問題關鍵的導引。

至關重要的，測試代碼應視為第一個項目成品，與實現代碼維持同等級別的質量要求，沒有重複。開發人員在提交程序單元代碼時一併提交單元測試代碼到代碼庫。徹底的極限編程單元測試代碼提供上述單元測試的收益，如簡化和更可信的程序開發和重構、簡化代碼集成、精確的文檔和模塊化的設計。而且，單元測試經常作為複合測試的一種形式被運行。

### 技術

單元測試通常情況下自動進行，但也可被手動執行。IEEE沒有偏愛某一種形式。手動的單元測試可用於step-by-step的教學文檔。儘管如此，單元測試的目標是隔離程序單元並驗證其正確性。自動執行使目標達成更有效，也可獲得本文上述單元測試收益。相反，不細心規劃或者精心的單元測試可能被視為包括多個軟體組件的集成測試案例，於是將因未完全達到建立單元測試的預定目標，測試可能失去較多收益。

在自動化測試時，為了實現隔離的效果，測試將脫離待測程序單元（或代碼主體）本身固有的運行環境之外，即脫離產品環境或其本身被創建和調用的上下文環境，而在測試框架中運行。以隔離方式運行有利於充分顯露待測試代碼與其它程序單元或者產品數據空間的依賴關係。這些依賴關係在單元測試中可以被消除。

藉助於自動化測試框架，開發人員可以抓住關鍵進行編碼並通過測試去驗證程序單元的正確性。在測試案例執行期間，框架通過日誌記錄了所有失敗的測試準則。很多測試框架可以自動標記和提交失敗的測試案例總結報告。根據失敗的程度不同，框架可以中止後續測試。

總體說來，單元測試會激發程式設計師創造解耦的和內聚的代碼體。單元測試實踐有利於促進健康的軟體開發習慣。設計模式、單元測試和重構經常一起出現在工作中，藉助於它們，開發人員可以生產出最為完美的解決方案。

### 單元測試框架

單元測試框架通常是沒有作為編譯器套件的第三方產品。他們幫助簡化單元測試的過程，並且已經為各種程式語言開發。

通常在沒有特定框架支援下，透過撰寫在測試中的執行單元，並使用斷言、異常處理、或其他控制流程機制來表示失敗的使用者代碼（client code）執行單元測試是可行的。不透過框架的單元測試有用之處在於進行單元測試時會有一個入行障礙；進行一點單元測試幾乎不比沒做好多少，但是一旦使用了框架，加入單元測試相對來說會簡單許多。在某些框架中許多先進單元測試特徵遺失了或者必須是手工編寫的。

### 語言層單元測試支持

某些程式語言直接支持單元測試。他們的語法允許直接進行單元測試的宣告而不需要匯入（不管是第三方的或標準的）。除此之外，單元測試的布爾條件可以用與非單元測試碼的布爾表示法相同的語法來表示，例如if和while宣告的用法。

直接支持單元測試的語言包含了：

- C#
- D語言

## 外部連結

- The evolution of Unit Testing Syntax and Semantics
- Unit Testing Guidelines from GeoSoft
- Test Driven Development (Ward Cunningham's Wiki)
- Unit Testing 101 for the Non-Programmer
- Step-by-Step Guide to JPA-Enabled Unit Testing (Java EE)
- libunittest:可移植的C++庫進行單元測試中使用新的C++11標準