0%

Android 平台集成 JUnit4 + Mockito + PowerMock + Robolectric 单元测试框架最佳实践

JUnit4MockitoPowerMockitoRobolectric 是一个牛逼的组合,在写单元测试用例时简直溜得飞起。通过 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, 需要在测试的 Modulebuild.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 不支持这个配置.

引用

  • 本文作者: 6x
  • 本文链接: https://6xyun.cn/article/135
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-ND 许可协议。转载请注明出处!