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

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, 需要在测试的 Modulebuild.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.

    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 许可协议。转载请注明出处!