2016年11月25日 星期五

Testing: 單元測試簡介

前言

先前有先寫一篇如何利用Android Studio建立Unit/Instrument測試與報告,主要是關於Unit/Instrument在Android Studio中的環境建立、執行與報告的產生,但完全沒有提到Android單元測試(Unit Test)的內容,所以想寫一些相關的議題,一方面作為筆記,另一方面也提供想實作不得其門而入的朋友們,透過循序漸進的方式了解。

什麼是單元測試

在軟體開發的領域中,它並不是新奇的概念與名詞,早在SmallTalk語言的開始,單元測試就已經出現,而且一次次地被驗證能成為開發人員提升軟體品質和深入理解系統功能需求的最佳方法之一。

那什麼是單元測試?一個單元測試就是一段程式碼,通常這段程式碼會是一個方法(或稱函式),這段程式碼也會呼叫另一段程式碼,然而為了要去檢驗它,你會做些假設並且斷定它的正確性。如果假設是錯誤的,那單元測試就失敗了。從這個描述中,我們能得知一個單元測試會有個測試的目標或對象,這個目標我們則稱為SUT(System Under Test)或是CUT(Class Under Test / Code Under Test)。從呼叫系統的一個公開方法(public method)到所預期的最終結果,這段系統產生的行為狀態,我們就能稱為一個單元(Unit)。再更白話點,物件導向的領域中,一個單元就是一個類別內的方法,然後你寫了一個測試程式去斷定這個方法的正確性或是能否正常工作,那這就是單元測試。

單元測試的特性

但是,上面描述的只是基本的概念,更明確的單元測試則是包含以下特性。
  • 單元測試必須是自動化,可重複執行的。
  • 如果單元測試失敗了,我們應該很容易發現預期的結果,並且得知問題所在。
  • 單元測試要容易實現的。
  • 它應該要獨立於其他測試的運行。
  • 無論執行多少次,都是穩定的結果。
  • 測試程式要完全控制測試的單元。
  • 任何人都能一鍵執行它。
  • 單元測試的速度是很快的。
你可以試著反問自己,如果你的單元測試很難達到上述條件,或是任何的測試,執行速度不快,結果不穩定,或者要用到被測單元的一個或多個真實環境或依賴物件,那你做的有可能就是整合測試了(例如:你測試時真的需要一個資料庫內的物件或是真實的檔案物件。)。

所謂的整合測試也是對一個工作單元進行測試,但這個測試會使用一到多個真實的依賴物(例如:網路、時間、資料庫、隨機數字...等等)。若覺得上述特性很難記憶,另一種簡單的方式叫做F.I.R.S.T準則。
  • Fast:快速。
  • Independent:獨立。
  • Repeatable:可重複。
  • Self-Validating:可反應驗證結果。
  • Timely:及時。

測試目標

對於單元測試的結果斷定,會涉及下列三種形式。
  • 預期被測物件的回傳值:被呼叫的公開方法會回傳一個值。
  • 預期被測物件的的狀態:在方法被呼叫的前後,被測物件的狀態或行為有一定的變化,這種變化無需查詢私有狀態就可判斷。(例如:一個登入物件,在一個會員登入後,其相對應的狀態應由未登入轉為已登入。)
  • 預期被測物件與相依物件的互動:當呼叫了一個不受測試控制的外部系統或物件,其不返回任何值,或返回值都被忽略。(例如:呼叫一個void方法,然而這個方法使用了一個物件,你預期這個方法呼叫後,該物件的某個方法理應被呼叫。)

簡單的單元測試案例

或許你聽過XUnit系列的測試框架(Framework),這系列都是以它們為主的語言開頭字母加上Unit作為名字,像是Java的單元測試框架就是JUnit、C++的單元測試框架則是CppUnit、.Net的單元測試框架稱為NUnit。先不論我們是否使用XUnit系列的框架,平常我們也會為程式撰寫單元測試,因為程式開發者在將軟體產品遞交給測試工程師驗證時,也都會執行基本的測試,這些也是單元測試的一種,例如:我們有可能會撰寫類似下面的測試,來觀察程式是否如預期。

public class SimpleParser {

    public int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        SimpleParser simpleParser = new SimpleParser();

        int expectedResult = 14;
        int actualResult = simpleParser.sum(5, 9);

        if (expectedResult == actualResult) {
            System.out.println("Success!");
        } else {
            System.out.println("Fail");
        }
    }

}

這段程式只是做簡單的加法,會預期 5 + 9 = 14,如果實際結果(actualResult)如同預期結果(expectedResult),那就顯示成功,這也是不依賴測試框架的單元測試。

撰寫單元測試的時機

然而,上述方式寫起來很麻煩,因此我們會使用單元測試框架來將上面的測試案例撰寫更為容易與便利,這也是在下個章節會提到的。稍微理解單元測試後,接下來就是關於何時該寫單元測試?過去,開發者可能會在軟體完成時才進行單元測試,但也越來越多人願意在程式撰寫前就先寫單元測試,我們則稱為「測試驅動開發」(Test-Driven Development, TDD)。測試驅動開發的過程,會先以撰寫失敗的測試案例為主,接著撰寫產品程式碼並測試,再重構或來改進設計,不斷重複這個過程,步驟與細節流程如下。





當然,撰寫單元測試是我們關注的議題,至於是否要導入測試驅動開發,則視公司和團隊性質去討論的,這邊就不多著墨。但是可以確定的是,至少當你在程式、需求有異動,或是已經出現非預期執行結果時,你必須要改變設計或建立重構時,就是該寫單元測試的時機。

小結

以上提到了單元測試、特性與時機,也知道單元測試與整合測試的區別,接下來我們就該多練習撰寫單元測試,習慣單元測試,讓你在早期發現軟體的缺陷,提升軟體的品質。雖然單元測試無法保證你的軟體完全不會有問題,卻能反映在你有測試到的部分都會是沒有問題且穩定的,而且其他開發者也能從你的測試案例中,得知系統的需求,快快加入單元測試的世界吧!