用Robolectric來作Android unit testing

做爲一隻本科非計算機專業的程序猿,手動寫單元測試是我歷來沒接觸過的東西,甚至在幾個月前,我都不知道單元測試是什麼東西。倒不是說沒聽過這個詞,也不是不知道它的大概是什麼東西——「用來測試一個方法,或者是一小塊代碼的測試代碼」。然而真正是怎麼作的?我並無一個概念,或者說並無一個感受。
記得第一份工做在創新工場的時候,聽當時的boss @王明禮 說,公司有個神級的程序員(。。。名字忘了。。。),他會寫大量的單元測試,甚至達到50%。當時崇拜之極,卻仍然以爲寫單元測試是很麻煩的一件事情。 java

扯遠了,話說回來,當你接觸多了國外的技術博客,視頻以後,你會發現,單元測試甚至TDD,在國外是很是流行的事情。不少人甚至說離開了單元測試,他們便沒有辦法寫代碼。這些都讓我對單元測試的好感度逐漸的上升。然而,真正讓我下定決心,必定要研究一下這個東西的,是前段時間看大名鼎鼎的《重構:改善現有代碼的藝術》裏面的一段話:android

I've found that writing good tests greatly speeds my programming, even if I'm not refactoring. This was a surprise for me, and it is counterintuitive for many programmers...
--Martin Fowler 《Refactoring: Improving the Design of Existing Code》git

是的,你沒看錯,他說單元測試能夠節約時間,提升開發速度!!!身爲一個無可救藥的懶癌患者,看了這句話簡直就像看到了一道神光似的!既然均可以節省時間,那確定是要看看的啊!程序員

有趣的是,Martin Fowler在《重構》裏面說他最初是由於 Dave Thomas說的一句話,讓他走上了單元測試的不歸路。而我這幾天恰好又在看Dave Thomas寫的《Programming Ruby 1.9 & 2.0》。。。。。。寫到這裏頓時以爲本身很不要臉。。。。。。
Martin Fowler在《重構》裏面還解釋了爲何單元測試能夠節省時間,大意是咱們寫程序的時候,其實大部分時間不是花在寫代碼上面,而是花在debug上面,是花在找出問題到底出在哪上面,而單元測試能夠最快的發現你的新代碼哪裏不work,這樣你就能夠很快的定位到問題所在,而後給以及時的解決,這也能夠在很大程度上防止regression(相信QE和QA們必定很喜歡哈哈。。。),這也是個大部分程序員和測試都很痛恨的問題。
以後不久,就開始花了點時間瞭解了一下Android裏面怎麼作unit testing,結果卻發現那是個很是難辦的事情。。。<!--more-->github

爲何android unit testing很差作

咱們知道安卓的app須要運行在delvik上面,咱們開發Android app是在JVM上面,在開發以前咱們須要下載各個API-level的SDK的,下載的每一個SDK都有一個android.jar的包,這些能夠在你的android_sdk_home/platforms/下面看到。當咱們開發一個項目的時候,咱們須要指定一個API-level,其實就是將對應的android.jar 加到這個項目的build path裏面去。這樣咱們的項目就能夠編譯打包了。然而如今的問題是,咱們的代碼必須運行在emulator或者是device上面,說白了,就是咱們的IDE和SDK只提供了開發和編譯一個項目的環境,並無提供運行這個項目的環境,緣由是由於android.jar裏面的class實現是不完整的,它們只是一些stub,若是你打開android.jar下面的代碼去看看,你會發現全部的方法都只有一行實現:
throw RuntimeException("stub!!」);
而運行unit test,說白了仍是個運行的過程,因此若是你的unit test代碼裏面有android相關的代碼的話,那運行的時候將會拋出RuntimeException("stub!!」)。爲了解決這個問題,如今業界提出了不少不一樣的程序架構,好比MVP、MVVM等等,這些架構的優點之一,就是將其中一層抽出來,變成pure Java實現,這樣作unit testing就不會遇到上面這個問題了,由於其中沒有android相關的代碼。
好奇的童鞋可能會問了,既然android.jar的實現是不完整的,那爲何咱們能夠編譯這個項目呢?那是由於編譯代碼的過程並無真正的運行這些代碼,它只會檢查你的接口有沒有定義,以及其餘的一些語法是否是正確。舉個簡單的例子:架構

public class Test {
    public static void main(String[] argv) {
        testMethod();
    }
    public static void testMethod() {
        throw RuntimeException("stub!!」);
    }
}

上面的代碼你一樣能夠編譯經過,但你運行的時候,就會拋出異常RuntimeException("stub!!」)。當咱們的項目運行在emulator或者是device上面的時候,android.jar被替換成了emulator或者是device上面的系統的實現,那上面的實現是真正實現了那些方法的,因此運行起來沒有問題。
話說回來,MVP、MVVM這些架構模式雖然解決了部分問題,能夠測試項目中不含android相關的類的代碼,然而一個項目中仍是有很大部分是android相關的代碼的,因此上面那種解決方案,實際上是放棄了其中一大塊代碼的unit test。
固然,話說回來,android仍是提供了他本身的testing framework,叫instrumentation,可是這套框架仍是繞不開剛剛提到的問題,他們必須跑在emulator或者是device上面。這是個很慢的過程,由於要打包、dexing、上傳到機器、運行起來界面。。。這個相信你們都有體會,尤爲是項目大了之後,運行一次甚至須要一兩分鐘,項目小的話至少也要十幾秒或幾十秒。以這個速度是沒有辦法作unit test的。
那麼怎麼樣便可以給android相關的代碼作測試,又能夠很快的運行這些測試呢?app

Robolectric to the rescue

解決的辦法就是使用一個開源的framework,叫robolectric,他們的作法是經過實現一套JVM能運行的Android代碼,而後在unit test運行的時候去截取android相關的代碼調用,而後轉到他們的他們實現的代碼去執行這個調用的過程。舉個例子說明一下,好比android裏面有個類叫TextView,他們實現了一個類叫ShadowTextView。這個類基本上實現了TextView的全部公共接口,假設你在unit test裏面寫到
String text = textView.getText().toString();。在這個unit test運行的時候,Robolectric會自動判斷你調用了Android相關的代碼textView.getText(),而後這個調用過程在底層截取了,轉到ShadowTextViewgetText實現。而ShadowTextView是真正實現了getText這個方法的,因此這個過程即可以正常執行。
除了實現Android裏面的類的現有接口,Robolectric還作了另一件事情,極大地方便了unit testing的工做。那就是他們給每一個Shadow類額外增長了不少接口,能夠讀取對應的Android類的一些狀態。好比咱們知道ImageView有一個方法叫setImageResource(resourceId),然而並無一個對應的getter方法叫getImageResourceId(),這樣你是沒有辦法測試這個ImageView是否是顯示了你想要的image。而在Robolectric實現的對應的ShadowImageView裏面,則提供了getImageResourceId()這個接口。你能夠用來測試它是否是正確的顯示了你想要的Image.框架

Talk is cheap. Show me the code!

下面簡單的介紹一下使用Robolectric來作unit testing。注意:下面的配置方法指的是AndroidStudio上面的,Eclipse用戶自行google一下配製方法。
要使用Robolectric,須要作幾步配置工做。dom

  1. 首先須要將它和JUnit4加到你項目的dependencies裏面,ide

testCompile 'junit:junit:4.12'  
testCompile ’org.robolectric:robolectric:3.0-rc3’

其中的Robolectric的最新版本號可能會變,具體能夠上jcenter查看一下當前的最新版本號。

  1. Build Variant裏面的Test Artifact選擇爲Unit Test,若是你找不到Build Variant,能夠在菜單欄選擇View -> Tool Windows -> Build Variant. 正常狀況下它會出如今左下角。

  2. 若是是Mac的話,還須要配置一個東西,菜單欄選擇 Run -> Edit Configuration -> Defaults -> JUnit,在Configuration tab將working directory改爲$MODULE_DIR$。這個配置是Robolectric官方文檔提到的,但我用最新的AndroidStudio1.3實驗的時候,忘了配置這個,貌似也能夠正確運行,anyway,配置一下也無所謂。具體見Robolectric的官方文檔,最下面那部分。

到這裏,就能夠開始code了。
測試代碼是放在app/src/test下面的,test class的位置最好跟target class的位置對應,好比MainActivity放在
app/src/main/java/com/domain/appname/MainActivity.java
那麼對應的test class MainActivityTest最好放在
app/src/test/java/com/domain/appname/MainActivityTest.java
這裏舉個簡單又稍微有點用的例子,假設app裏面有兩個Activity:MainActivitySecondActivityMainActivity裏面有一個TextView,點擊一下這個TextView將跳轉到SecondActivityMainActivity裏面的代碼大概以下:

public class MainActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
  
        TextView textView = (TextView)findViewById(R.id.textView1);  
        textView.setOnClickListener(new OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                startActivity(new Intent(MainActivity.this, SecondActivity.class));  
            }  
        });  
    }  
}

對應的測試類,MainActivityTest的代碼:

@RunWith(RobolectricGradleTestRunner.class)  
@Config(constants = BuildConfig.class, sdk = 21)  
public class MainActivityTest {  
    @Test  
    public void testMainActivity() {  
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);  
        mainActivity.findViewById(R.id.textView1).performClick();  
          
        Intent expectedIntent = new Intent(mainActivity, SecondActivity.class);  
        ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);  
        Intent actualIntent = shadowActivity.getNextStartedActivity();  
        Assert.assertEquals(expectedIntent, actualIntent);  
    }  
}

上面的代碼測試的就是當用戶點擊textView的時候,程序會正確的跳轉到SecondActivity。其中@RunWith(RobolectricGradleTestRunner.class)表示用Robolectric的TestRunner來跑這些test,這就是爲何Robolectric能夠檢測到你調用了Android相關的類,而後截取這些調用,轉到他們的Shadow類的緣由。此外,@Config用來配置一些東西。
代碼中的
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);用來建立MainActivity的instance,或者說,用來啓動這個Activity,當Robolectric.setupActivity返回的時候,這個Activity已經完成了onCreate、onStart、onResume這幾個生命週期的回調了。
mainActivity.findViewById(R.id.textView1).performClick();用來觸發點擊事件。ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);用來獲取mainActivity對應的ShadowActivity的instance。
shadowActivity.getNextStartedActivity();用來獲取mainActivity調用的startActivity的intent。這也是正常的Activity類裏面不具備的一個接口。
最後,調用Assert.assertEquals來assert啓動的intent是咱們指望的intent。
運行這個unit test,啓動命令行,cd到項目的根目錄,運行
./gradlew test ,幾秒鐘後,你將看到測試運行的結果

...  
:app:preCompileReleaseUnitTestJava  
:app:preReleaseUnitTestBuild UP-TO-DATE  
:app:prepareReleaseUnitTestDependencies  
:app:processReleaseUnitTestJavaRes UP-TO-DATE  
:app:compileReleaseUnitTestJava UP-TO-DATE  
:app:compileReleaseUnitTestSources UP-TO-DATE  
:app:assembleReleaseUnitTest UP-TO-DATE  
:app:testRelease UP-TO-DATE  
:app:test UP-TO-DATE  
  
BUILD SUCCESSFUL  
  
Total time: 1.45 secs

在個人機器上(MacBook Air 2013款,8G內存,算比較低的配置),運行這個test只須要不到2秒鐘,這纔是TDD該有的速度。
注:第一次運行可能須要下載一些library,或者是gradle自己,可能須要花一點時間,這個跟unit test自己沒關。
整個項目已經放到github上面:robolectric-demo

小結

整體來講,Robolectric是個很是強大好用的unit testing framework。雖然使用的過程當中確定也會遇到問題,我我的就遇到很多問題,尤爲是跟第三方的library好比Retrofit、ActiveAndroid結合使用的時候,會有很多問題,但瑕不掩瑜,咱們依然能夠用它完成很大部分的unit testing工做。

有任何意見或建議,或者發現文中任何問題,歡迎留言評論!

做者 小創 更多文章 | Github | 公衆號

相關文章
相關標籤/搜索