Android UnitTest 介绍和引入
后续会写几篇与 Android 测试有关的文章。之所以准备写,是因为在国内做 Android 开发的程序员很少在开发过程中,在写完功能程序的前提下,自己写用例程序来测试自己的程序,主要还是依靠测试人员来测试(国内的测试人员,绝大多数做的是不符合软件测试程序专业程序的)。而恰恰一些功能的边界测试或者特殊场景测试,一般测试人员是无法测试到的,只有通过造数据的方式才能测试到。
下面就看下 Android 中的一些测试的基本信息。
Android 测试类型
按功能测试分:
- 功能测试:功能是否按照预期执行。
- (无障碍)性能测试:功能是否高效执行完成。
- 兼容性测试:功能是否能够在所有设备和 API 级别上正常运行。
按照测试范围分能:
- 单元测试/小型测试:测试一个类或方法。
- 端到端测试/大型测试:测试整个屏幕或者用户体验流程。
- 中型测试:介于上述两者之间,用于检查两个或更多单元之间的集成。
上述两种分类方式是 Android 官网上的分类方式。
对开发者来讲,从 Android 工程结构上看,是将代码写在 src/test
目录中还是 src/androidTest
目录中。这样可以分成 单元测试 和 界面测试。
单元测试代码只是基于 JVM 的代码测试,可以不运行到模拟器或真机环境中。
界面测试,需要测试代码运行到模拟器或真机上。
另外两个概念是:
- 小型本地测试:可以使用本地运行的 Android 模拟器(基于 JVM 模拟 Android 环境),例如 Robolectric。
- 小型插桩测试:您可以验证代码能否很好地支持框架功能(例如 SQLite 数据库)。
下面开始分析我在项目中的基本情况。
软件版本
这篇文章中列举的是我在当前项目中使用的相关环境及软件版本,后续可能会随着项目的发展对软件版本做出修改。
首先来说明下当前项目的环境及 IDE 版本等信息。
工具 | 版本 | 说明 |
---|---|---|
Ubuntu | 24.04.2 LTS | 稳定版本 |
Android Studio Meerkat Feature Drop | 2024.3.2 Patch 1 | 最新稳定版 |
AGP (Android Gradle Plugin) | 7.4.2 | |
Gradle | 7.6.4 | 当前项目可使用的最高版本,受到其他必须使用的库的限制。 |
KGP (Kotlin Gradle Plugin) | 1.9.0 | |
Kotlin | 1.9.0 | |
JDK | 17 | |
junit | 4.13.2 | |
robolectric | 4.14.1 | 结合 AGP 7.4.2 运行,可运行的版本有 4.14.1,4.13,4.12.2,4.12.1,4.11,4.10.3。 |
引入 JUnit4 和 Robolectric
在写单元测试前,先按测试需要配置引入 JUnit4 和 Roboletric 库。
// Gradle
dependencies {
// Test libs 基本库
testImplementation "androidx.test:core:1.5.0" // AndroidX Test library
testImplementation "androidx.test.ext:junit-ktx:1.2.1"
testImplementation "junit:junit:4.13.2" // JUnit4 的最新稳定版
testImplementation "org.robolectric:robolectric:4.14.1" // 最新稳定版
}
这里配置的是 JUnit4 而非 JUnit5,主要在于 Android 官方目前在介绍使用 Robolectric 时使用的是 JUnit4 来作为最佳实践,即 robolectric 底层的基础实现是 JUnit4,与 JUnit5 的兼容性目前不得而知。
下面看下 JUnit5 和 JUnit4 的部分介绍。
JUnit5 和 JUnit4
JUnit5 到现在,版本已经发展到 5.13.0 版本,而 JUnit4 已经在维护阶段,最后的 JUnit4 版本是 4.13.2。
JUnit5 是当前最新测试框架。它基于 JVM,聚焦在 Java 8 及以上版本,且在实现的设计上更加灵活。相比于 JUnit4,JUnit5 提供了更加强大的功能和更好的扩展性,当前它也是主流。
下面来看下两者在 Android 测试中特性。
特性 | JUnit4 | JUnit5 |
---|---|---|
参数化测试 | 需要依赖外部库(如 Theroies) | 原生支持 |
动态测试 | 不支持 | 原生支持 |
扩展模式 | 有限 | 强大且灵活 |
Java 版本支持 | Java 7+ | Java 8+ |
Android 兼容性 | 完全兼容 | 单元测试兼容,界面测试需 JUnit4 |
项目一开始准备的是 JUnit5 和 JUnit4 混用,但是遇上问题。
JUnit5 和 JUnit4 混用问题
在 sdk/build.gradle
文件中做了 JUnit5 和 JUnit4 的配置。
dependencies {
// .....
// Test libs
testImplementation "androidx.test:core:1.5.0" // AndroidX 测试库
testImplementation "androidx.test.ext:junit-ktx:1.2.1"
// JUnit5 API
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.3'
// JUnit Jupiter 引擎(运行 JUnit5 测试)
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3")
testImplementation "junit:junit:4.13.2"
// JUnit Vintage 引擎(运行 JUnit4 测试)
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.3")
testImplementation "org.robolectric:robolectric:4.14.1"
}
Junit5 不再是单个模块就可以解决问题的了,它分成 Platform, Jupiter, Vintage 3 个模块。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
配置文件中简单写了作用,各模块更详细的作用可以点击 JUnit5 User Guide 了解更多。
由于测试的方法中有依赖 android.content.Context
类,因此还引入了 robolectric 库
第一个问题:运行任务 sdk:testDebugTestUnit
一直执行,不返回任何结果。
出现这个问题时,运行的测试代码如下。
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.R])
class ContextTest {
private lateinit var _context: Context
@Before
fun init() {
val application = ApplicationProvider.getApplicationContext<Context>()
println("init(): Application=$application")
_context = application.applicationContext
}
@Test
fun test_getEntropyPool_shouldReturnRespGetEntropyPool() {
val deviceId = "9ijuyh7wolkirwfs"
println("test_getEntropyPool_shouldReturnRespGetEntropyPool(): $deviceId")
Assert.assertNotNull(deviceId)
}
}
这个问题可能原因中分析是 JUnit4,JUnit5 混用,导致 test runner 启动时无法正确启动。
第二个问题:错误提示 No instrumentation registered! Must run under a registering instrumentation.
运行的测试代码
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.R])
class ContextTest {
private lateinit var _context: Context
companion object {
@BeforeClass
@JvmStatic
fun init() {
val application = ApplicationProvider.getApplicationContext<Context>()
println("init(): Application=$application")
_context = application.applicationContext
}
}
@Test
fun test_getEntropyPool_shouldReturnRespGetEntropyPool() {
val deviceId = "9ijuyh7wolkirwfs"
println("test_getEntropyPool_shouldReturnRespGetEntropyPool(): $deviceId")
Assert.assertNotNull(deviceId)
}
}
这个问题原因标注 @BeforeClass
的方法会在所有 @Test
方法执行之前先执行,但这时 Robolectric 还没有初始化完成,ApplicationProvider
等也无法直接执行。
综述
如果单元测试选择 JUnit5(已是事实的主流),在 Android 如果只有单元测试可以使用 JUnit5,但如果还有界面测试的,选择 JUnit4,不要混用 JUnit5,其一 Robolectric 与 JUnit5 没什么兼容性,其支持的还是 JUnit4。其二是 Android 官网在讲述测试中,使用是 JUnit4。