這是一系列安卓單元測試的文章,目測主要會cover如下的主題:html
什麼是單元測試java
爲何要作單元測試android
JUnitgit
Mockito程序員
Robolectricgithub
Dagger2數據庫
一個具體的app例子實踐網絡
神祕的bonusapp
首先須要介紹一下什麼是單元測試。不少人像我同樣,本科並非計算機專業出身的,若是在職的公司不要求作單元測試的話,可能對這個詞並無一個確切的概念。而即便是計算機專業出身,若是畢業之後寫的很少的話,可能對這個詞的含義也不是很清楚。從名字上看,單元測試是爲了測試某一個代碼單元而寫的測試代碼。可是什麼叫「一個代碼單元」呢?是一個模塊、仍是一個類、仍是一個方法(函數)呢?不一樣的人、不一樣的語言,都有不一樣的理解。通常的定義,尤爲是是在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); } }
這裏的CalculatorTest
是Calculator
對應的測試類。而這裏的testAdd()
就是add()
這個方法對應的測試方法。因此,寫單元測試,就是給你的每一個類的每一個public方法寫對於的測試方法。非public方法咱們通常是不測試的,雖然能夠經過反射等手段去作,可是通常看來,非public方法是這個類的實現細節,咱們並不關心,咱們只關心某一個public方法的輸入、輸出。
通常來講,一個方法對應的測試方法主要分爲3部分,以上面的測試方法爲例:
setup。通常是new出你要測試的那個類,以及其餘一些前提條件的設置:Calculator calculator = new Calculator();
執行操做。通常是調用你要測試的那個方法,得到運行結果:int sum = calculator.add(1, 2);
驗證結果。驗證獲得的結果跟預期中是同樣的:Assert.assertEquals(3, sum);
通常來講,咱們寫單元測試,會用到一些單元測試框架。常見的Java單元測試框架有JUnit、TestNG等等。在這個系列的文章中,我採用JUnit 4,這也是用的最多的一個測試框架。上面的第三部,Assert.assertEquals(3, sum);
用的就是JUnit裏面的驗證結果的方法,最多見的就是調用Assert
類的一些assert方法。除了上面用到的assertEquals
,還有assertTrue
, assertNotNull
等等。關於JUnit,我會在後面的系列文章中專門介紹。
咱們知道,在一個android gradle project中,源代碼默認是放在src/main/java下面的。而對應的單元測試代碼則是放在src/test/java下面的,以下圖所示:
其中的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裏面看到。大體以下圖:
這篇文章的代碼在github上,感興趣的能夠clone下來本身試試。
這裏須要強調一個觀念,那就是單元測試只是測試一個方法單元,它不是測試一整個流程。舉個例子來講,一個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的,那麼怎麼驗證這個方法是正確的呢?其實仔細想一想,這個方法也是有輸出的,它的輸出就是,調用了mUserManager
的performLogin
方法,同時傳給他兩個參數。因此只要驗證mUserManager
的performLogin
方法獲得了調用,同時傳給他的參數是正確的,就說明這個方法是能正常工做的。
那怎麼樣驗證這個Activity的login()
方法,會調用mUserManager
的performLogin
方法呢?這裏涉及到mock的概念,在後面的文章關於Mockito的使用的時候,會介紹到,這裏簡單解釋下,那就是在測試環境下,咱們會使用一套mock framework(Mockito),生成一個mock的UserManager
而後賦值給mUserManager
,由於這個mUserManager
是經過mock framework生成的,因此這個mock framework能夠驗證它的什麼方法被調用了,參數是什麼,等等。
上面講述了單元測試的定義,以及與集成測試的區別,通常來講,單元測試不會接觸到數據庫,不會接觸到網絡,不會接觸到一些複雜的外部環境,若是有的話,那多是你測試的方式有誤,測試的粒度不夠「單元」,但願這篇文章能將這二者的區別解釋清楚。
有任何意見或建議,或者發現文中任何問題,歡迎留言!
最後,若是你也對安卓單元測試感興趣的話,歡迎加入咱們的交流羣: