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官网