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
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
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
文件中添加如下配置:
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
中如下配置即可(这里以阿里云镜像仓库为例):
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', ''
}
}
}
使用
搭建环境
- 普通用例环境
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();
}
}
- 参数化用例环境
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();
}
}
使用环境
- 普通测试用例
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);
}
}
- 参数化测试用例
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
. -
运行时报错
Failed calling method
如上面版本配套关系中所描述, 基本是版本配套的问题. -
运行时报错
java.lang.NullPointerException
, 使用的版本是Robolectric 3.x
设置android.testOptions.unitTests.includeAndroidResources = false
, 看起来是Robolectric 3.x
不支持这个配置.