跳轉到內容

軟體工程/測試/單元測試簡介

來自華夏公益教科書,開放的世界開放的書籍

在計算機程式設計中,單元測試是一種方法,透過該方法對原始碼的各個單元進行測試,以確定它們是否適合使用。單元是應用程式中最小的可測試部分。在程序式程式設計中,單元可以是單個函式或過程。單元測試由程式設計師建立,有時也由白盒測試人員建立。

理想情況下,每個測試用例都是獨立的:可以使用方法存根、模擬物件、[1] 假象和測試工具來幫助隔離測試模組。單元測試通常由軟體開發人員編寫和執行,以確保程式碼滿足其設計並按預期執行。它的實現可以從非常手動(紙筆)到作為構建自動化的正式部分。

好處

[edit | edit source]

單元測試的目的是隔離程式的每個部分,並證明各個部分是正確的。[2] 單元測試提供了一個嚴格的書面契約,程式碼必須滿足該契約。因此,它提供了幾個好處。單元測試在開發週期的早期發現問題。

促進變化

[edit | edit source]

單元測試允許程式設計師在以後重構程式碼,並確保模組仍然正常工作(例如,在迴歸測試中)。該過程是對所有函式和方法編寫測試用例,以便每當更改導致故障時,都可以快速識別和修復該故障。

隨時可用的單元測試使程式設計師能夠輕鬆地檢查程式碼是否仍然正常執行。

在持續的單元測試環境中,透過持續維護的固有實踐,單元測試將繼續準確地反映可執行檔案和程式碼的預期用途,以應對任何更改。根據既定的開發實踐和單元測試覆蓋範圍,可以保持最新準確性。在單元測試中,我們分別測試每個模組。

簡化整合

[edit | edit source]

單元測試可以減少單元本身的不確定性,並且可以用於自下而上的測試風格方法。透過首先測試程式的各個部分,然後測試其各部分的總和,整合測試變得更加容易。

單元測試的複雜層次結構並不等於整合測試。與外圍單元的整合應包含在整合測試中,但不包含在單元測試中。[需要引用] 整合測試通常仍然嚴重依賴人工手動測試;高階或全域性範圍的測試可能難以自動化,因此手動測試通常看起來更快更便宜。[需要引用]

文件

[edit | edit source]

單元測試提供了一種關於系統現狀的文件。希望瞭解單元提供的功能以及如何使用它的開發人員可以檢視單元測試,以瞭解單元 API 的基本知識。

單元測試用例體現了對單元成功至關重要的特性。這些特性可以表明單元的適當/不當使用,以及單元要捕獲的負面行為。單元測試用例本身記錄了這些關鍵特性,儘管許多軟體開發環境並不僅僅依賴於程式碼來記錄正在開發的產品。

相比之下,普通的敘述性文件更容易偏離程式的實現,從而變得過時(例如,設計變更、功能蔓延、保持文件最新的鬆懈做法)。

設計

[edit | edit source]

當使用測試驅動方法開發軟體時,單元測試可以代替正式的設計。每個單元測試都可以被視為一個設計元素,指定類、方法和可觀察的行為。以下 Java 示例將有助於說明這一點。

這是一個測試類,它指定了實現中的許多元素。首先,必須有一個名為 Adder 的介面,以及一個名為 AdderImpl 的帶有零引數建構函式的實現類。它繼續斷言 Adder 介面應該有一個名為 add 的方法,該方法具有兩個整型引數,並返回另一個整數。它還指定了此方法對於一小部分值的行為。

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);
    }
}

在這種情況下,單元測試先於編寫,充當一個設計文件,指定了所需解決方案的形式和行為,而不是實現細節,這些細節留給程式設計師。遵循“執行最簡單可能的有效操作”的實踐,透過使測試透過的最簡單的解決方案如下所示。

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

與其他基於圖的設計方法不同,使用單元測試作為設計具有一項重大優勢。設計文件(單元測試本身)可用於驗證實現是否符合設計。使用單元測試設計方法,如果開發人員沒有根據設計實現解決方案,則測試將永遠無法透過。

單元測試確實缺乏圖表的某些可訪問性,但 UML 圖表現在可以透過免費工具(通常作為 IDE 的擴充套件提供)輕鬆地為大多數現代語言生成。基於 xUnit 框架的免費工具將圖形呈現檢視以供人類消費,外包到另一個系統。

分離介面和實現

[edit | edit source]

由於某些類可能引用其他類,測試一個類經常會蔓延到測試另一個類。一個常見的例子是依賴資料庫的類:為了測試該類,測試人員經常編寫與資料庫互動的程式碼。這是一個錯誤,因為單元測試通常不應該超出其自身的類邊界,尤其是不要跨越這樣的程序/網路邊界,因為這會導致單元測試套件出現不可接受的效能問題。跨越這樣的單元邊界會將單元測試變成整合測試,並且當測試用例失敗時,會難以確定哪個元件導致了失敗。另見模擬、模擬和整合測試

相反,軟體開發人員應該圍繞資料庫查詢建立一個抽象介面,然後使用自己的模擬物件實現該介面。透過將這種必要的附加關係從程式碼中抽象出來(暫時降低了淨有效耦合),可以比以前更徹底地測試獨立單元。這將導致一個質量更高、更易於維護的單元。

單元測試侷限性

[edit | edit source]

不能指望測試能發現程式中的所有錯誤:除了最簡單的程式之外,幾乎不可能評估所有執行路徑。單元測試也是如此。此外,根據定義,單元測試僅測試單元本身的功能。因此,它不會發現整合錯誤或更廣泛的系統級錯誤(例如跨多個單元執行的功能或非功能測試區域,如效能)。單元測試應該與其他軟體測試活動一起進行。像所有形式的軟體測試一樣,單元測試只能表明錯誤的存在;它們不能表明錯誤不存在。

軟體測試是一個組合問題。例如,每個布林決策語句至少需要兩個測試:一個結果為“真”,一個結果為“假”。因此,對於編寫的每一行程式碼,程式設計師通常需要 3 到 5 行測試程式碼。[3] 這顯然需要時間,並且其投資可能不值得付出努力。還有很多問題根本無法輕鬆測試——例如那些是非確定性的或涉及多個執行緒的問題。此外,編寫單元測試程式碼與測試的程式碼一樣可能出現錯誤。弗雷德·布魯克斯在《人月神話》中引用道:永遠不要帶兩塊計時器出海。總是帶一塊或三塊。 意思是,如果兩個計時器矛盾,你怎麼知道哪個是正確的?

為了從單元測試中獲得預期的收益,在整個軟體開發過程中需要嚴格的紀律。除了要仔細記錄已執行的測試之外,還需要記錄對軟體的該單元或任何其他單元的原始碼所做的所有更改。使用版本控制系統至關重要。如果單元的後期版本無法透過它以前透過的特定測試,版本控制軟體可以提供自那時起應用於單元的原始碼更改(如果有)的列表。

同樣重要的是要實施一個可持續的流程,以確保每天審查測試用例失敗並立即解決。[4] 如果沒有實施這樣的流程並將其融入團隊的工作流程,應用程式將隨著單元測試套件的演變而失去同步,從而增加誤報並降低測試套件的有效性。

應用

[edit | edit source]

極限程式設計

[edit | edit source]

單元測試是極限程式設計的基石,它依賴於自動化的單元測試框架。這個自動化的單元測試框架可以是第三方,例如 xUnit,也可以是開發團隊內部建立的。

極限程式設計使用建立單元測試進行測試驅動開發。開發人員編寫一個單元測試,以暴露軟體需求或缺陷。此測試將失敗,因為需求尚未實現,或者因為它故意暴露了現有程式碼中的缺陷。然後,開發人員編寫最簡單的程式碼以使測試(以及其他測試)透過。

系統中的大多數程式碼都經過單元測試,但並非一定經過所有程式碼路徑測試。極限程式設計要求“測試所有可能出錯的東西”的策略,而不是傳統的“測試所有執行路徑”方法。這導致開發人員比傳統方法開發的測試更少,但這並不是什麼問題,更多的是對事實的重述,因為傳統方法很少被系統地遵循,以至於所有執行路徑都經過了徹底的測試。[需要引用] 極限程式設計僅僅承認測試很少是詳盡的(因為它通常過於昂貴和耗時,在經濟上不可行),並提供有關如何有效地集中有限資源的指導。

至關重要的是,測試程式碼被認為是頭等專案工件,這意味著它與實現程式碼保持相同的質量,並且消除了所有重複。開發人員與測試的程式碼一起將單元測試程式碼釋出到程式碼庫中。極限程式設計的徹底單元測試帶來了上述優勢,例如更簡單、更自信的程式碼開發和重構、簡化的程式碼整合、準確的文件以及更模組化的設計。這些單元測試也作為一種迴歸測試不斷執行。

技術

[edit | edit source]

單元測試通常是自動化的,但仍然可以手動執行。IEEE 並不偏袒一方。[5] 手動單元測試方法可以使用逐步說明性文件。然而,單元測試的目標是隔離單元並驗證其正確性。自動化對於實現這一點是有效的,並且可以實現本文中列出的許多好處。相反,如果沒有仔細計劃,粗心大意的手動單元測試用例可能會作為涉及許多軟體元件的整合測試用例執行,從而無法實現大多數(如果不是全部)為單元測試製定的目標。

為了在使用自動化方法時充分實現隔離的效果,被測試的單元或程式碼體在框架內執行,該框架位於其自然環境之外。換句話說,它是在其最初建立的產品或呼叫上下文之外執行的。以這種隔離的方式進行測試揭示了被測試程式碼與產品中的其他單元或資料空間之間的不必要的依賴關係。然後可以消除這些依賴關係。

使用自動化框架,開發人員將標準編碼到測試中以驗證單元的正確性。在測試用例執行期間,框架記錄任何標準都失敗的測試。許多框架還將自動標記這些失敗的測試用例並將其彙總報告。根據失敗的嚴重程度,框架可能會停止後續測試。

因此,單元測試傳統上是程式設計師建立解耦和內聚程式碼體的動力。這種實踐促進了軟體開發中的健康習慣。設計模式、單元測試和重構經常協同工作,從而可能出現最佳解決方案。

單元測試框架

[edit | edit source]

單元測試框架通常是第三方產品,不會作為編譯器套件的一部分進行分發。它們有助於簡化單元測試過程,因為它們是為各種語言開發的。測試框架的示例包括開源解決方案,如各種程式碼驅動測試框架(統稱為 xUnit),以及專有/商業解決方案,如 TBrun、Testwell CTA++ 和 VectorCAST/C++。

通常可以在沒有特定框架支援的情況下執行單元測試,方法是編寫練習被測試單元的客戶端程式碼,並使用斷言、異常處理或其他控制流機制來發出失敗訊號。沒有框架的單元測試很有價值,因為採用單元測試的門檻很高;很少的單元測試幾乎比沒有單元測試好不了多少,而一旦框架到位,新增單元測試就變得相對容易。[6] 在某些框架中,許多高階單元測試功能缺失或必須手動編碼。

語言級單元測試支援

[edit | edit source]

一些程式語言直接支援單元測試。它們的語法允許直接宣告單元測試,而無需匯入庫(無論是第三方庫還是標準庫)。此外,單元測試的布林條件可以用與非單元測試程式碼中使用的布林表示式相同的語法來表達,例如用於ifwhile語句的語法。

直接支援單元測試的語言包括

  • Cobra
  • D
  1. Fowler, Martin (2007-01-02). "模擬不是存根". 檢索於 2008-04-01.
  2. Kolawa, Adam (2007). 自動化缺陷預防:軟體管理最佳實踐. Wiley-IEEE 計算機協會出版社. p. 75. ISBN 0470042125. {{cite book}}: |pages= 和 |page= 指定了多個 (幫助); 未知引數 |coauthors= 被忽略了 (|author= 建議) (幫助)
  3. Cramblitt, Bob (2007-09-20). "Alberto Savoia 讚揚軟體測試". 檢索於 2007-11-29.
  4. daVeiga, Nada (2008-02-06). "無需恐懼地更改程式碼:利用迴歸安全網". 檢索於 2008-02-08.
  5. IEEE 標準委員會, "IEEE 軟體單元測試標準:美國國家標準,ANSI/IEEE Std 1008-1987"IEEE 標準:軟體工程,第二卷:過程標準;1999 版;由電氣與電子工程師協會出版 IEEE 計算機學會軟體工程技術委員會.
  6. Bullseye 測試技術 (2006–2008). "中等覆蓋率目標". 檢索於 2009 年 3 月 24 日. {{cite web}}: 檢查 |date= 中的日期值 (幫助); 文字“publication-place”被忽略 (幫助)
[編輯 | 編輯原始碼]
華夏公益教科書