Android單元測試(一): 首先,從是什麼開始

這是一系列安卓單元測試的文章,目測主要會cover以下的主題:

  1. 什麼是單元測試
  2. 爲什麼要做單元測試
  3. JUnit
  4. Mockito
  5. Robolectric
  6. Dagger2
  7. 一個具體的app例子實踐
  8. 神祕的bonus

什麼是單元測試

首先需要介紹一下什麼是單元測試。很多人像我一樣,本科並不是計算機專業出身的,如果在職的公司不要求做單元測試的話,可能對這個詞並沒有一個確切的概念。而即使是計算機專業出身,如果畢業以後寫的不多的話,可能對這個詞的含義也不是很清楚。從名字上看,單元測試是爲了測試某一個代碼單元而寫的測試代碼。但是什麼叫“一個代碼單元”呢?是一個模塊、還是一個類、還是一個方法(函數)呢?不同的人、不同的語言,都有不同的理解。一般的定義,尤其是是在OOP領域,是一個類的一個方法。在此,我們也這樣理解:單元測試,是爲了測試某一個類的某一個方法能否正常工作,而寫的測試代碼。
我們舉一個例子說明一下,假如你有一個類,定義如下:

public class Calculator {
    public int add(int one, int another) {
        //爲了簡單起見,暫不考慮溢出等情況。
        return one + another;
    }
}

那麼爲了測試這個Calculator類的add()方法,我們可以寫如下的單元測試代碼:

public class CalculatorTest {
    public void testAdd() throws Exception {
        Calculator calculator = new Calculator();
        int sum = calculator.add(1, 2);
        Assert.assertEquals(3, sum);
    }
}

這裏的CalculatorTestCalculator對應的測試類。而這裏的testAdd()就是add()這個方法對應的測試方法。所以,寫單元測試,就是給你的每個類的每個public方法寫對應的測試方法。非public方法我們一般是不測試的,雖然可以通過反射等手段去做,但是一般看來,非public方法是這個類的實現細節,我們並不關心,我們只關心某一個public方法的輸入、輸出。
一般來說,一個方法對應的測試方法主要分爲3部分,以上面的測試方法爲例:

  1. setup。一般是new出你要測試的那個類,以及其他一些前提條件的設置:Calculator calculator = new Calculator();
  2. 執行操作。一般是調用你要測試的那個方法,獲得運行結果:int sum = calculator.add(1, 2);
  3. 驗證結果。驗證得到的結果跟預期中是一樣的:Assert.assertEquals(3, sum);

一般來說,我們寫單元測試,會用到一些單元測試框架。常見的Java單元測試框架有JUnitTestNG等等。在這個系列的文章中,我採用JUnit 4,這也是用的最多的一個測試框架。上面的第三部,Assert.assertEquals(3, sum); 用的就是JUnit裏面的驗證結果的方法,最常見的就是調用Assert類的一些assert方法。除了上面用到的assertEquals,還有assertTrueassertNotNull等等。關於JUnit,我會在後面的系列文章中專門介紹。

如何在一個android project裏面運行單元測試

我們知道,在一個android gradle project中,源代碼默認是放在src/main/java下面的。而對應的單元測試代碼則是放在src/test/java下面的,如下圖所示:

ut src position

其中的package name可以隨意定,很多人喜歡跟src package name保持一致,我個人習慣在最後加上.test後綴,因爲AndroidStudio太智能了,經常我需要重命名單元測試的package的時候,AndroidStudio會把src的package也給重命名了。
打開CalculatorTest,用鼠標右鍵點擊testAdd()方法,選擇Run testAdd(), 如下圖所示:

從圖中你可以看出,你可以按快捷鍵Ctrl+Shift+R快速運行,當然,這要求你的光標當前焦點是在這個方法內部的。如果你的焦點是在類內部,而不在某一個測試方法內部,那麼Ctrl+Shift+R將運行這個測試類的所有測試方法。當然,你也可以通過右鍵點擊測試類名來運行這個測試類裏面的所有測試方法。
運行結束以後,你會在底部的“Run”這個tab看到運行的結果,如下圖所示:

除了在AndroidStudio裏面運行,你還可以在命令行通過gradle testDebugUnitTest,或者是gradle testReleaseUnitTest,分別運行debug和release版本的unit testing,這種方式可以一次性運行所有測試類的所有測試方法。 這種方式的運行結果如下圖所示:

每個test case的報告可以在project_root/app/build/reports/tests/debug/index.html 這個xml裏面看到。大致如下圖:

單元測試不是集成測試

這裏需要強調一個觀念,那就是單元測試只是測試一個方法單元,它不是測試一整個流程。舉個例子來說,一個Login頁面,上面有兩個輸入框和一個button。兩個輸入框分別用於輸入用戶名和密碼。點擊button以後,有一個UserManager會去執行performlogin操作,然後將結果返回,更新頁面。
那麼我們給這個東西做單元測試的時候,不是測這一整個login流程。這種整個流程的測試:給兩個輸入框設置正確的用戶名和密碼,點擊login button, 最後頁面得到更新。叫做集成測試,而不是單元測試。當然,集成測試也是有他的必要性的,然而這不是我們每個程序員應該花多少精力所在的地方。在這方面,有一個理論叫做Test Pyramid,如下圖所示:

Test Pyramid理論基本大意是,單元測試是基礎,是我們應該花絕大多數時間去寫的部分,而集成測試等應該是冰山上面能看見的那一小部分。
爲什麼是這樣呢?因爲集成測試設置起來很麻煩,運行起來很慢,發現的bug少,在保證代碼質量、改善代碼設計方面更起不到任何作用,因此它的重要程度並不是那麼高,也無法將它納入我們正常的工作流程中。
而單元測試則剛好相反,它運行速度超快,能發現的bug更多,在開發時能引導更好的代碼設計,在重構時能保證重構的正確性,因此它能保證我們的代碼在一個比較高的質量水平上。同時因爲運行速度快,我們很容易把它納入到我們正常的開發流程中。
至於爲什麼集成測試發現的bug少,而單元測試發現的bug多,這裏也稍作解釋,因爲集成測試不能測試到其中每個環節的每個方面,某一個集成測試運行正確了,不代表另一個集成測試也能運行正確。而單元測試會比較完整的測試每個單元的各種不同的狀況、臨界條件等等。一般來說,如果每一個環節是對的,那麼在很大的概率上,整個流程就是對的。雖然不能保證整個流程100%一定是對的。所以,集成測試需要有,但應該是少量,單元測試是我們應該花重點去做的事情。
那對於這個例子,單元測試是怎麼樣的呢?這個請看下一小節。

兩種函數(方法),兩種不同的測試方式

一個類的方法可以分爲兩種,一種是有返回值的,另一種是沒有返回值的。對於有返回值的方法,我們要測起來比較容易,就跟上面的Calculator例子那樣,輸入相應的參數,得到相應的返回值,然後驗證得到的返回值跟我們預期的值一樣,就好了。
但是沒有返回值的方法,要怎麼測試呢?比如說剛剛login的例子,點擊那個按鈕,會執行Activity的login()方法,它的定義如下:

public void login() {
    String username = ...//get username from username EditText
    String password = ...//get password from password EditText
    //do other operation like validation, etc
    ...

    mUserManager.performlogin(username, password);
}

這個方法是void的,那麼怎麼驗證這個方法是正確的呢?其實仔細想想,這個方法也是有輸出的,它的輸出就是,調用了mUserManagerperformLogin方法,同時傳給他兩個參數。所以只要驗證mUserManagerperformLogin方法得到了調用,同時傳給他的參數是正確的,就說明這個方法是能正常工作的。
那怎麼樣驗證這個Activity的login()方法,會調用mUserManagerperformLogin方法呢?這裏涉及到mock的概念,在後面的文章關於Mockito的使用的時候,會介紹到,這裏簡單解釋下,那就是在測試環境下,我們會使用一套mock framework(Mockito),生成一個mock的UserManager然後賦值給mUserManager,因爲這個mUserManager是通過mock framework生成的,所以這個mock framework可以驗證它的什麼方法被調用了,參數是什麼,等等。

小結

上面講述了單元測試的定義,以及與集成測試的區別,一般來說,單元測試不會接觸到數據庫,不會接觸到網絡,不會接觸到一些複雜的外部環境,如果有的話,那可能是你測試的方式有誤,測試的粒度不夠“單元”,希望這篇文章能將這兩者的區別解釋清楚。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章