Robolectric官網java
當你在用JUnit測一些android類時,會發現測試失敗:java.lang.RuntimeException: Stub!android
若是你沒關聯android源碼,你點進去看android相關類,會發現不少方法的實現就是一行throw new RuntimeException("Stub!")。studio只爲咱們提供了開發、編譯Android代碼的環境。但studio沒有提供運行環境,若是要運行app,只能在模擬器或者真機上,到時這些方法會被替換成android rom裏面相同的類的相同方法。git
而JUnit只能用於測試能在jvm上跑的純java代碼,因此,哪怕你的代碼裏,只有一行Log的日誌輸出,都會拋這樣的異常。github
也許MVP這些架構,能將大部分的java代碼與android代碼進行隔離,好比presenter的代碼,基本上就是純java代碼。但view層、model層的實現,每每仍是有不少android代碼,好比view層的控件,model層的數據庫(底層實現是android版的SQLite)。數據庫
Robolectric測試框架,能必定程度解決了這種困擾。它的設計思路即是經過實現一套jvm能運行的android代碼,從而作到脫離android環境進行測試。Robolectric有一些shadow類,使用它們,能夠替換掉android相關類,代替它們在jvm上運行。但這種替換,也須要耗費必定的時間。因此,使用Robolectric的測試,運行起來,都須要十秒左右,雖比不上普通的單元測試,但也比跑真機快多了。windows
Robolectric的集成比較麻煩,若是集成過程當中,有遇到的bug,能夠參考文末。儘可能使用新版本的Robolectric,能夠避免不少沒必要要的麻煩。api
android {
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation "org.robolectric:robolectric:4.3"
}
複製代碼
注意:瀏覽器
1.若是要使用Robolectric4.0以上版本,必需要studio 3.2以上。bash
2.若是少了includeAndroidResources = true
這個配置,用Robolectric測試UI時,會報相似的錯誤:架構
android.content.res.Resources$NotFoundException: String resource ID #0x7f0b001f
複製代碼
將Working directory的值設置爲$MODULE_DIR$
。
java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
複製代碼
或者以下警告:
No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
複製代碼
在VM options裏面添加一行"-noverify"。
不然在運行測試方法過程當中,可能碰見以下異常:java.lang.VerifyError: Expecting a stackmap frame at branch target 384
Exception Details:
Location:
cn/jpush/android/service/PushReceiver.<clinit>()V @11: goto
Reason:
Expected stackmap frame at this location.
Bytecode:
0x0000000: 1023 bd00 3c59 0312 1710 ffa7 0175 5359
0x0000010: 0412 0b03 a701 6c53 5905 1216 04a7 0163
複製代碼
Downloading: org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 2K from sonatype
Downloading: org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 98508K from sonatype
複製代碼
能夠嘗試試一下:http://repo1.maven.org/maven2/
拼接上 org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.jar
,而後用該連接在瀏覽器上直接下載(http不行的話,試試https),再將下載到的jar包扔到 相似C:\Users\lenovo\.m2\repository\org\robolectric\android-all\8.0.0_r4-robolectric-r1
的目錄裏面。而後重啓studio,從新運行測試便可。
@RunWith(RobolectricTestRunner.class)
//一般,@Config這一行配置能夠不要
//@Config(constants = BuildConfig.class)
public class RobolectricTest {
......
}
複製代碼
constants = BuildConfig.class
在某些低版本的Robolectric中須要,但在最新的版本中已經不須要了。在低版本的Robolectric中,須要使用該類中的常量來計算Gradle在構建項目時使用的輸出路徑。若是沒有這些值,Robolectric將沒法找到合併的manifest, resources和assets。
只須要在@Before方法裏,預先設置ShadowLog.stream = System.out便可,這樣,咱們代碼裏的log,單元測試裏的log都會重定向,輸出到控制檯,方便咱們調試。如:
@Before
public void setup() {
ShadowLog.stream = System.out;
}
複製代碼
注意:
1)一個好的單元測試,並不須要日誌來支撐。即咱們要習慣使用斷言,而不是經過看日誌來判斷測試是否成功。有時候,輸出日誌只是爲了更方便咱們調試而已。
2)測試時,日誌輸出設置,在咱們自定義的Application裏面,是不起效果的。不過在其餘地方,不受影響。估計是Robolectric裏面源碼執行順序的問題。
RuntimeEnvironment.application是Robolectric模擬android環境生成的Application實例。在你使用Robolectric寫測試的時候,你隨時可使用它,做爲全局Context。
這也側面說明一個問題,使用Robolectric寫測試的時候,Application是必定會被初始化的。因此若是你有自定義的Application,那麼Robolectric一定會初始化你的自定義Application。若是你的自定義Application有一些初始化動做,好比第三方庫的加載,Robolectric執行不了,可能會拋異常,那就影響測試結果了。這時候,在項目實踐裏,可使用下面的配置@Config(application = Application.class)
,繞過自定義Application,直接讓Robolectric初始化系統的Application。
@RunWith(RobolectricTestRunner.class)
@Config(application = Application.class)
public class NotInitYourApplicationTest {
...
}
複製代碼
項目實踐裏,我會使用下面的基類,當某個測試類須要使用Robolectric幫助寫測試代碼時,繼承該基類就好了,不用每次都重複前面所說的日誌輸出、繞過自定義Application等繁瑣的配置。其中,RobolectricRule,繼承自JUnit的TestRule類,裏面封裝了日誌輸出等設置代碼。
@RunWith(RobolectricTestRunner.class)
@Config(application = Application.class)
public class RobolectricTest {
@Rule
public RobolectricRule mRobolectricRule = new RobolectricRule();
}
複製代碼
Shadow是Robolectric的核心。Robolectric裏,定義了大量對應android源碼的shadow類。shadow類,如其名「影子」。當一個android類的方法被調用的時候,Robolectric就會嘗試尋找該類相應的影子類,替代對應的android類,執行相應的方法。
下面一個簡單的自定義Shadow類的例子:
public class Person {
public String sayHello() {
return "I'm myself!";
}
}
@Implements(Person.class)
public class ShadowPerson {
@Implementation
public String sayHello() {
return "I'm shadow!";
}
}
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowPerson.class}, manifest = Config.NONE)
public class ShadowTest {
@Test
public void sayHello() {
Person person = new Person();
assertEquals("I'm shadow!", person.sayHello());
}
}
複製代碼
結果顯示,person.sayHello()調用到的是ShadowPerson的sayHello()方法。這就是Shadow。
固然,其背後的原理是很複雜的。使用到了MethodHandle。我也看不懂,就不廢話了。
能夠參考資料:Robolectric Shadow類實現方式探索
在Robolectric4.0以前,Robolectric也能夠用於在src/test下面,對activity、fragment等進行測試。可是,功能極其有限,寫起activity、fragment相關測試代碼,操做也很繁瑣。相關的使用,這裏就不介紹了,官網本身都把舊版的使用介紹徹底剔除了。感興趣的能夠本身上網搜索相關資料。(Robolectric官網的教程,以前每隔幾個版本就大變樣,無力吐槽)
在Robolectric4.0以後,Robolectric對Android官方測試庫進行了兼容。在這之後,推薦使用AndroidX Test的API寫測試代碼。也就是說你能夠在src/test下面,使用Espresso的api寫相關測試。(關於Espresso的使用,將會在淺談測試之Espresso裏詳細介紹)
1)引入espresso的相關依賴
testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
testImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'androidx.test.ext:junit:1.1.1'
複製代碼
2)測試代碼
//@RunWith(AndroidJUnit4.class)代替@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
@Config(application = Application.class)
public class MainActivityTest {
@Rule
public IntentsTestRule<MainActivity> intentsTestRule = new IntentsTestRule<>(MainActivity.class, true, false);
@Before
public void setup() {
AppComponent appComponent = DaggerAppComponent.builder()
.build();
ComponentHolder.setAppComponent(appComponent);
intentsTestRule.launchActivity(new Intent(ApplicationProvider.getApplicationContext(), MainActivity.class));
}
@Test
public void testClickButton() {
//使用Espresso的API
onView(withId(R.id.btn_jump)).perform(ViewActions.click());
intended(IntentMatchers.hasComponent(LoginActivity.class.getName()));
//在Robolectric裏,沒法真的跳轉到LoginActivity
onView(withId(R.id.tv_login)).check(doesNotExist());
}
}
複製代碼
注意:
1)使用@RunWith(AndroidJUnit4.class)代替@RunWith(RobolectricTestRunner.class)
2)目前只建議使用Robolectric單獨測試Activity、Fragment。參考issue。
當在MainActivity裏調用startActivity去開啓LoginActivity的時候,最終會調用Instrumentation.execStartActivity()方法。但該類有一個對應的shadow類:ShadowInstrumentation。ShadowInstrumentation裏一樣有個execStartActivity()方法,它會覆蓋Instrumentation.execStartActivity()方法,而它並不會真的去啓動LoginActivity。
實際上,試驗發現,不只Activity之間的跳轉,若是在當前Activity顯示一個Dialog,也沒法用espresso的api驗證Dialog的信息,意思是Dialog也不會真的初始化,顯示出來。想一想也是,畢竟Robolectric只是一個Unit Testing Framework。因此,我的並不傾向用Robolectric測試Activity、Fragment裏面的代碼。由於Activity、Fragment的業務邏輯,一般已經抽取到MVP的P層,或者mvvm的vm層。Activity、Fragment裏的代碼一般都是控件交互、頁面交互,已經算是功能測試的範圍了。一般會在src/androidTest裏面,使用Espresso寫相關的功能測試、端對端測試。
那麼Robolectric一般用於哪裏?能夠用在Presenter層、Model層這些地方的單元測試上面,提供一個模擬的Android運行環境,避免一些Android類,影響咱們測試。固然,也建議Presenter層、Model層這些地方,儘可能避免沒必要要的Android代碼。
官方教程:AndroidX Test
這裏使用到的數據框架是room,再配合上rxjava2。
1)添加room的測試支持庫
testImplementation "android.arch.core:core-testing:1.1.0"
複製代碼
2)限於篇幅緣由,只貼出部分關鍵的測試代碼。
public class PersonDaoTest extends RobolectricTest {
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
private Person tony = new Person("1", "tony", Sex.MALE);
private Person marry = new Person("2", "marry", Sex.FEMALE);
private PersonDatabase database;
private PersonsDao personsDao;
@Before
public void setup() {
//使用內存數據庫。數據將會在測試用例運行過程當中被保存,在進程結束後消失。
database = Room.inMemoryDatabaseBuilder(RuntimeEnvironment.application,
PersonDatabase.class)
//容許在主線程進行查詢。注意,僅用於測試。
.allowMainThreadQueries()
.build();
personsDao = database.personDao();
}
@After
public void clear() {
database.close();
}
@Test
public void savePerson_getPerson() {
personsDao.insertPersion(tony);
personsDao.getPersonById("1")
.test()
.assertNoErrors()
//不會完成。由於room是響應式的,會繼續觀察數據庫的數據變化。
.assertNotComplete()
.assertValue(tony::equals);
}
}
複製代碼
參考資料:Using PowerMock
緣由:在默認狀況下,Robolectric會在你的manifest中指定的targetSdkVersion上運行你的代碼。好比使用的Robolectric3.4版本,不支持API level 26,不支持API level15及如下的sdk版本。
解決方法:
1)使用更高版本的Robolectric。
2)使用@Config的sdk
、minSdk
或maxSdk
配置屬性,指定你的sdk版本。如:
@Config(sdk = { JELLY_BEAN, JELLY_BEAN_MR1 })
public class YourTest {
}
複製代碼
參考:官方教程:Configuring Robolectric
解決方法:
testImplementation "org.robolectric:shadows-multidex:3.4-rc2"
複製代碼
異常詳細信息:
javax.net.ssl.SSLHandshakeException:
sun.security.validator.ValidatorException:
PKIX path validation failed:
java.security.cert.CertPathValidatorException:
Algorithm constraints check failed on signature algorithm: SHA256WithRSAEncryption
複製代碼
解決辦法:
testImplementation 'org.bouncycastle:bcprov-jdk15on:1.57'
複製代碼
參考:issue
Robolectric,在我看來,這是一個有點被神化的測試框架。由於不少博文介紹它,都是相似「在android設備上跑測試用例太慢,而它能在JVM上跑測試用例」。林林總總,很容易讓咱們陷入誤區,覺得要儘量地使用它,並避免在android設備上跑測試用例。
其實否則,它雖然有必定的好處,能幫助咱們不用經過註釋原有代碼裏的android類,好比日誌輸出等,就能夠在jvm上運行咱們的測試用例。但也有缺陷:
第一,集成困難。尤爲是對初學者來講。實在是太多坑了。
第二,有必定的侷限性。歸根結底,它只是作了一些mock而已。它只是經過實現一套jvm能運行的android代碼,從而作到脫離android環境進行測試,並非真正的android源碼。就像帶着鐐銬跳舞同樣。有時候,仍是會碰到各類奇奇怪怪的小bug。
因此,對我我的而言,更多的時候都是使用它來配合Mockito、PowerMock測試Presenter、Model裏面的方法,避免由於Presenter、Model裏使用到了一小部分android代碼而影響測試代碼的運行。
注意:
1)Robolectric的源碼,貌似沒法在windows上編譯成功。當初找了老半天緣由。唔...後來在官網找到了說法:
We develop Robolectric on Mac and Linux. You might be able to figure out how to get it to work on Windows if you really want to for some reason. Good luck.
2)因爲篇幅有限,不可能也不必列出Robolectric的全部測試示例,以及各類使用姿式。
文中的相關測試例子,以及更多的測試例子都可以在UnitTest裏面找到。
更多的測試例子,請參考Robolectric源碼裏面的測試用例,以及官方給出的robolectric-samples 。
更詳細的教程,請參考:Robolectric官網