JUnit4
、Mockito
、PowerMockito
和 Robolectric
是一个牛逼的组合,在写单元测试用例时简直溜得飞起。通过 PowerMockito
弥补 Mockito
测试框架不能 mock
静态方法
、final方法
和 private方法
的不足,通过 Robolectric
可以实现在 JVM
中就可以很方便的调用 Android
相关的类和方法,相比 Android
官方的真机测试解决方案,那爽了不止一点点。
版本配套关系
在实际使用时发现, JRE
+ AGP
+ PowerMock
+ Robolectric
存在一定的配套关系, 如果他们之间的版本不匹配, 轻则不能运行, 重则报莫名其妙的错误, 这里罗列一下笔者遇到的版本配套失败的例子:
JRE
AGP
PowerMock
Robolectric
故障
1.8
4.x
2.0.9
4.6.1
运行时编译报错 "IllegalArgumentException, message: Unsupported class file major version 59.", AGP 不能识别并转换 bcprov-jdk15on 的类文件字节码
1.8
4.x
2.0.9
4.4.1
运行时报错 "java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8)", Robolectric 模拟 Q/29 至少需要 JRE9
1.8
4.x
1.6.6
3.8
运行依赖 PowerMock 的参数化用例时报错 "Failed calling method", 根本原因未知
最终总结出几套可以满足使用场景的版本配套关系:
| JRE | AGP | PowerMock | Robolectric | 最大支持SDK | 描述 |
|-|-|-|-|-|-|
|11|7.x|2.0.9|4.6.1|R/30|最新的一组, 支持模拟的SDK也最新, 且 AGP 已经限制必须使用 JRE11|
|11|4.x|2.0.9|4.4.1|Q/29|AGP 4.x 最高配套 Robolectric 4.4.1, 最大支持模拟 Q/29|
|1.8|4.x|2.0.9|4.4.1|P/28|Robolectric 4.4.1 没有 JRE9 支撑, 最高支持 P/28|
|1.8|4.x|1.6.6|3.6.2|O_MR1/27|PowerMock 1.6.6 配套的 Robolectric 版本不能大于 3.6.2, 否则依赖 PowerMock 的参数化用例不能执行, 且最高支持 O_MR1/27|
另外, Robolectric 4.x
使用了 AndroidX
的单元测试组件.
引入依赖
Robolectric 4.x + PowerMock 2.x
1 2 3 4 5 6 testImplementation 'junit:junit:4.+' testImplementation 'androidx.test:core:1.4.0' testImplementation "org.robolectric:robolectric:4.6.1" testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.9' testImplementation 'org.powermock:powermock-classloading-xstream:2.0.9'
Robolectric 3.x + PowerMock 1.x
1 2 3 4 5 testImplementation 'junit:junit:4.+' testImplementation "org.robolectric:robolectric:3.6.2" testImplementation 'org.powermock:powermock-api-mockito2:1.6.6' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.6' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.6'
配置
includeAndroidResources
如果使用的 Robolectric
版本是 4.x
, 需要在测试的 Module
的 build.gradle
文件中添加如下配置:
1 2 3 4 5 6 7 8 9 android { ... testOptions { unitTests { includeAndroidResources = true } } ... }
这不是必选的, 但是建议配置上; 如果不进行配置, 在 Robolectric 4.x
可能会导致一些与环境有关的用例失败.
但如果使用的 Robolectric 3.x 就不要配置或设置为 false
, 否则运行时会报错误.
自定义 Maven
仓库
Robolectric
在初始化的时候需要下载并加载它的 影子库类
, 实现 Android 11 SDK
的库类大小已经有将近 120M
, 需要在 mavenCentral
仓库下载且不受 repositories
配置控制, 如果你的网络不能连接外网或出境访问缓慢, 可以通过下面的方式自定义下载仓库:
这里踩了坑, 最开始是通过继承 RobolectricTestRunner
静态设置系统属性的方式实现, 结果发现参数化运行器使用的是内部的 Runner
, 这个办法就走不通了, 最终在 Robolectric
官方网站找到了最优雅的方法.
在模块的 build.gradle
中如下配置即可(这里以阿里云镜像仓库为例):
1 2 3 4 5 6 7 8 9 10 11 12 android { testOptions { // MavenRoboSettings.java // http://robolectric.org/configuring/#system-properties unitTests.all { systemProperty 'robolectric.dependency.repo.id', 'aliyun' systemProperty 'robolectric.dependency.repo.url', 'https://maven.aliyun.com/repository/public/' systemProperty 'robolectric.dependency.repo.username', '' systemProperty 'robolectric.dependency.repo.password', '' } } }
使用
搭建环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import android.app.Application; import android.content.Context; import android.os.Build; // Robolectric 3.x not required import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Rule; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; @RunWith(RobolectricTestRunner.class) /** * @see org.robolectric.internal.SdkConfig * @see org.robolectric.plugins.DefaultSdkProvider */ @Config(sdk = Build.VERSION_CODES.R) @PowerMockIgnore({"org.robolectric.*", "org.powermock.*", "org.mockito.*", "android.*", "androidx.*", "org.json.*", "sun.security.*", "javax.net.*"}) public abstract class RobolectricTest { @Rule public PowerMockRule rule = new PowerMockRule(); @Before public void setUpRobolectricTest() { ShadowLog.stream = System.out; } public Application getApplication() { // Robolectric 3.x // return (Application) ShadowApplication.getInstance().getApplicationContext(); return ApplicationProvider.getApplicationContext(); } public Context getContext() { return getApplication(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import android.app.Application; import android.content.Context; import android.os.Build; // Robolectric 3.x not required import androidx.test.core.app.ApplicationProvider; import org.junit.Before; import org.junit.Rule; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLog; @RunWith(ParameterizedRobolectricTestRunner.class) /** * @see org.robolectric.internal.SdkConfig * @see org.robolectric.plugins.DefaultSdkProvider */ @Config(sdk = Build.VERSION_CODES.R) @PowerMockIgnore({"org.robolectric.*", "org.powermock.*", "org.mockito.*", "android.*", "androidx.*", "org.json.*", "sun.security.*", "javax.net.*"}) public abstract class ParameterizedRobolectricTest { @Rule public PowerMockRule rule = new PowerMockRule(); @Before public void setUpRobolectricTest() { ShadowLog.stream = System.out; } public Application getApplication() { // Robolectric 3.x // return (Application) ShadowApplication.getInstance().getApplicationContext(); return ApplicationProvider.getApplicationContext(); } public Context getContext() { return getApplication(); } }
使用环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import static org.junit.Assert.*; import org.junit.Test; public class TestRobolectric extends RobolectricTest { @Test public void test() { assertNotNull(getApplication()); assertNotNull(getContext()); System.out.println(getContext().getPackageName()); System.out.println(getContext().getApplicationInfo().packageName); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import static org.junit.Assert.*; import org.junit.Test; import org.robolectric.ParameterizedRobolectricTestRunner; public class TestParameterizeRobolectric extends ParameterizedRobolectricTest { @ParameterizedRobolectricTestRunner.Parameters(name = "index:{index} value:[{0},{1}]") public static Collection<Object[]> data() { return Arrays.asList( new Object[]{1, "1"}, new Object[]{2, "2"}, new Object[]{3, "3"}, new Object[]{4, "4"}, new Object[]{5, "5"} ); } private int mValue1; private String mValue2; public TestParameterizeRobolectric(int value1, String value2) { mValue1 = value1; mValue2 = value2; } @Test public void test() { System.out.println("value1:" + mValue1); System.out.println("value2:" + mValue2); assertEquals(mValue1, Integer.parseInt(mValue2)); } }
踩坑记录
运行时报错 java.lang.NoClassDefFoundError: android/content/Context
最近在给一个项目替换 Robolectric
时发现一直报错这个错, 我检查了N遍没找到问题, 后面直接删掉了项目下 .idea
和 .gradle
的文件夹, 竟然奇迹的好了...
运行时报错 IllegalArgumentException, message: Unsupported class file major version 59.
项目使用的 AGP 4.x
+ Robolectric 4.6.1
, 如上面版本配套关系中所描述, 原因是 Robolectric 4.6.1
依赖的 org.bouncycastle:bcprov-jdk15on:1.68
使用 JDK 16
编译的, 导致 AGP 4.x
不能解析类文件的字节码. 解决办法那就是要么升级 AGP
要么降级 Robolectric
.
https://stackoverflow.com/questions/65182975/bouncycastle-android-unsupported-class-file-major-version-59-failed-to-transf
运行时报错 Failed calling method
如上面版本配套关系中所描述, 基本是版本配套的问题.
运行时报错 java.lang.NullPointerException
, 使用的版本是 Robolectric 3.x
设置 android.testOptions.unitTests.includeAndroidResources = false
, 看起来是 Robolectric 3.x
不支持这个配置.
引用