- 示例工程
【免费下载链接】testing-samples
A collection of samples demonstrating different frameworks and techniques for automated testing
本篇技术指南围绕 testing-samples 仓库中的 unit/BasicUnitAndroidTest 示例展开,讲解"在设备或模拟器上运行的单元测试(Android Unit Test,也称 Instrumented Unit Test)"的完整实践:它为何必须依赖 Android Framework、与本地单元测试及 UI 测试有何本质区别、如何在 Android Studio 与命令行中配置并运行,以及如何解读测试报告。读完本文,你将掌握以Parcelable为代表的需要真实 Android 运行时支撑的类的测试方法,并能直接复用在 Android 项目中。
什么是 Android Unit Test(Instrumented Unit Test)
官方仓库的 README 对这一概念给出了非常精确的定义:
AnUnit Android Testis a test that needs an Android device or emulator but it's different from a UI test because it doesn't start any activities.
也就是说,Android Unit Test 是一种需要 Android 设备或模拟器才能运行的测试,但它与 UI 测试(如 Espresso 测试)不同——它不会启动任何 Activity。它运行在设备上的androidTest测试进程里,因此可以访问 Android Framework 提供的真实 API,但又不像 UI 测试那样关注界面交互与用户操作路径。
三者对比可以这样理解:
| 测试类型 | 运行位置 | 是否需要设备 | 是否启动 Activity | 典型用途 |
|---|---|---|---|---|
| 本地单元测试(JVM Test) | 开发机 JVM | 否 | 否 | 纯逻辑、工具类、无 Android 依赖的代码 |
| Android Unit Test(本示例) | 设备/模拟器 | 是 | 否 | 依赖 Android Framework 的类(如Parcelable、SharedPreferences) |
| UI / Instrumentation 测试 | 设备/模拟器 | 是 | 是 | 界面交互、端到端流程 |
本示例恰好落在中间这一格:被测对象是实现了Parcelable接口的模型类,测试需要真实的android.os.Parcel机制,但测试本身不启动任何界面。
为什么这个测试无法脱离 Android 设备运行
这是本示例最核心的看点。README 中明确指出:
In this sample the test can't run without the Android Framework because the Parcel class is used in one of the methods of the Parcelable interface and the way data is written into a Parcel and read from it is not trivial enough to be stubbed.
被测类 LogHistory.java 是一个实现了Parcelable的日志模型,内部用List<Pair<String, Long>>保存"文本 + 时间戳"形式的日志条目。其writeToParcel方法(LogHistory.java 第 42-58 行)先将数据拆成String[]与long[]两个数组,依次写入Parcel:
@Override public void writeToParcel(Parcel out, int flags) { // Prepare an array of strings and an array of timestamps. String[] texts = new String[mData.size()]; long[] timestamps = new long[mData.size()]; // Store the data in the arrays. for (int i = 0; i < mData.size(); i++) { texts[i] = mData.get(i).first; timestamps[i] = mData.get(i).second; } // Write the size of the arrays first. out.writeInt(texts.length); // Write the two arrays in a specific order. out.writeStringArray(texts); out.writeLongArray(timestamps); }对应的反序列化逻辑则位于私有构造函数(LogHistory.java 第 91-114 行),由CREATOR字段的createFromParcel触发:先读入数组长度,再按写入顺序依次读取字符串数组与长整型数组,最后校验两个数组长度一致,否则抛出IllegalStateException。
这里的关键点在于:Parcel是 Android 系统进程间通信(IPC)与状态保存机制的底层载体,它涉及 Binder 驱动的序列化协议、栈式写入顺序、数据位置游标(dataPosition)等一套复杂的真实运行时行为。正如 README 所强调的,Parcel的写入与读取方式"trivial enough to be stubbed"的反面——它并不平凡到可以用 Mock 简单替换。因此这个测试必须运行在真实的 Android 设备或模拟器上,而不能像纯 JVM 单元测试那样在开发机本地直接执行。这正是把它放进androidTest目录而非test目录的根本原因:
Note that the unit test is placed in
/androidTest/instead of/test/.
项目结构:被测类、测试类与界面演示
本示例采用 Gradle 构建,模块结构清晰:
unit/BasicUnitAndroidTest/ ├── app/ │ ├── build.gradle │ └── src/ │ ├── androidTest/java/.../LogHistoryAndroidUnitTest.java # 设备端单元测试 │ └── main/ │ ├── java/.../LogHistory.java # 被测类(Parcelable 模型) │ ├── java/.../MainActivity.java # 界面演示,展示 Parcelable 实战 │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── settings.gradle └── gradlew / gradlew.batREADME 中建议按以下顺序阅读这三个类:
- LogHistory.java —— 被测类(class under test),实现
Parcelable; - LogHistoryAndroidUnitTest.java —— Android 单元测试本体;
- MainActivity.java —— 在真实界面中展示
Parcelable的用法,但请注意:测试本身不会在设备或模拟器上启动任何 Activity。
MainActivity是一个很好的"实际用途佐证":它允许用户向多行日志追加条目,并在设备旋转时通过onSaveInstanceState(MainActivity.java 第 88-91 行)把mLogHistory以outState.putParcelable(KEY_HISTORY_DATA, mLogHistory)的方式保存,再在onCreate中通过savedInstanceState.getParcelable(KEY_HISTORY_DATA)恢复。这正是Parcelable序列化机制在 Android 生命周期中的经典应用场景——LogHistory的单元测试保证了这一保存/恢复链路的数据完整性。
构建配置要点
模块级构建文件 app/build.gradle 中有几处与本示例直接相关的配置:
defaultConfig { applicationId "com.example.android.testing.unittesting.basicunitandroidtest" minSdkVersion 14 targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } dependencies { // Testing-only dependencies androidTestImplementation "androidx.test:core:" + rootProject.coreVersion; androidTestImplementation "androidx.test.ext:junit:" + rootProject.extJUnitVersion; androidTestImplementation "androidx.test:runner:" + rootProject.runnerVersion; androidTestImplementation "com.google.truth:truth:" + rootProject.truthVersion }testInstrumentationRunner显式声明了测试运行器为androidx.test.runner.AndroidJUnitRunner,这是设备端测试能够通过connectedCheck被调度的前提;- 测试相关依赖全部使用
androidTestImplementation作用域,表明它们只参与androidTest源集(即src/androidTest/)的编译与运行,不会被打进应用 APK 的主包; - 具体版本号集中在根构建文件 build.gradle 的
ext块中统一管理:coreVersion = "1.5.0"、extJUnitVersion = "1.1.4"、runnerVersion = "1.5.0"、truthVersion = "1.1.3",Android Gradle Plugin 版本为8.1.1。
在 Android Studio 中配置并运行测试
README 给出了在 IDE 中运行的完整六步流程:
- 下载项目代码,推荐使用
git clone方式获取 testing-samples 仓库; - 在 Android Studio 中选择File|Open...,指向项目的
./build.gradle文件打开工程; - 阅读相关源码:
LogHistory.java是被测类(实现Parcelable),LogHistoryAndroidUnitTest是 Android 单元测试,MainActivity.java展示Parcelable的实际用法——但再次强调,测试运行过程中不会在设备或模拟器上显示任何 Activity; - 创建自定义运行器的测试配置,运行器指定为
androidx.test.runner.AndroidJUnitRunner:- 打开Run菜单 |Edit Configurations
- 新增一个Android Tests配置
- 选择一个模块(本示例中即
app) - 添加Specific instrumentation runner:
androidx.test.runner.AndroidJUnitRunner
- 连接真机或启动模拟器;
- 运行新建的测试配置。
配置完成后,单元测试将自动执行,无需任何手动干预。需要说明的是,第 4 步的手动配置在常规工程中其实已被app/build.gradle里的testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"声明覆盖,IDE 配置主要是在需要临时指定不同运行器或测试筛选条件时使用;两步配置的目标是一致的。
在命令行中使用 Gradle 运行
不依赖 IDE 时,使用 Gradle Wrapper 在命令行运行是更贴近 CI 的方式。README 给出的命令非常简洁:
./gradlew connectedCheck其中:
connectedCheck是 Android Gradle Plugin 提供的聚合任务,它会连接所有已检测到的设备/模拟器,执行androidTest源集中的所有测试(等价于connectedAndroidTest及其变体);- Windows 环境请使用
gradlew.bat替代./gradlew; - 如果所有测试均通过,终端会输出
BUILD SUCCESSFUL消息,任务结束码为 0,适合直接接入持续集成流水线。
测试用例到底在验证什么
既然命令行会直接跑测试,不妨深入看一下 LogHistoryAndroidUnitTest.java 中唯一的测试方法logHistory_ParcelableWriteRead(第 48-68 行):
@RunWith(AndroidJUnit4.class) @SmallTest public class LogHistoryAndroidUnitTest { public static final String TEST_STRING = "This is a string"; public static final long TEST_LONG = 12345678L; private LogHistory mLogHistory; @Before public void createLogHistory() { mLogHistory = new LogHistory(); } @Test public void logHistory_ParcelableWriteRead() { // Set up the Parcelable object to send and receive. mLogHistory.addEntry(TEST_STRING, TEST_LONG); // Write the data Parcel parcel = Parcel.obtain(); mLogHistory.writeToParcel(parcel, mLogHistory.describeContents()); // After you're done with writing, you need to reset the parcel for reading. parcel.setDataPosition(0); // Read the data LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel); List<Pair<String, Long>> createdFromParcelData = createdFromParcel.getData(); // Verify that the received data is correct. assertThat(createdFromParcelData.size()).isEqualTo(1); assertThat(createdFromParcelData.get(0).first).isEqualTo(TEST_STRING); assertThat(createdFromParcelData.get(0).second).isEqualTo(TEST_LONG); } }这条测试链路完整模拟了"序列化 → 反序列化 → 数据比对"的闭环,其中三个细节值得注意:
Parcel.obtain()与setDataPosition(0):obtain()从全局池中取出一块可复用的Parcel缓冲区;写入完成后必须调用setDataPosition(0)把读取游标重置回起点,否则读取会从缓冲区末尾开始,直接导致数据错乱。这正是 README 所说"写入与读取并不 trivial"的直观体现;- 走
CREATOR.createFromParcel而非直接调用私有构造函数:反序列化走的是与 Android 系统一致的公开入口,验证的是真实的Parcelable.Creator契约; - 断言采用 Google Truth 风格:
assertThat(...).isEqualTo(...)可读性更高,测试类同时标注了@RunWith(AndroidJUnit4.class)与@SmallTest——前者指定 AndroidX 的 JUnit4 运行器,后者声明这是一个小规模(快速、无副作用)的测试,便于按规模分拣执行。
查看测试报告
命令行跑完./gradlew connectedCheck后,HTML 格式的测试报告会自动生成在:
app/build/outputs/reports/androidTests/connected该目录下通常包含index.html总览页面及各测试类的详细结果页面,会列出每个测试方法(如logHistory_ParcelableWriteRead)的执行状态、耗时与失败堆栈,可以直接用浏览器打开查看,也可以由 CI 系统作为构建产物归档。由于这是相对app模块的输出目录,对应的仓库根路径为 app/build/outputs/reports/androidTests/connected(该目录在构建后才会生成)。
小结:什么时候应该选择这种测试形态
从本示例可以提炼出一条清晰的选型判断:当被测代码强依赖 Android Framework 的真实实现、且该实现难以用 Mock/Stub 替代时(典型如Parcelable、Parcel、SharedPreferences、Context等),就应当把单元测试放进src/androidTest/,作为 Android Unit Test 在设备或模拟器上运行。它既保留了单元测试"不启动 Activity、专注单一逻辑单元"的轻量特性,又借助 Instrumentation 环境获得了真实系统服务的支撑。
实践时只需记住三条主线:目录放对(androidTest而非test)、运行器配好(AndroidJUnitRunner)、跑法选好(Android Studio 的 Android Tests 配置或./gradlew connectedCheck)。在此基础上,你就可以像本示例验证LogHistory的 Parcelable 契约一样,为自己的模型类、存储层与系统服务交互代码建立可靠的设备端单元测试防线。
- 示例工程
【免费下载链接】testing-samples
A collection of samples demonstrating different frameworks and techniques for automated testing
相关推荐
OpenSSL 单元测试实战指南:基于 cmocka 与 --wrap 的隔离测试体系(test/unit)
OpenSSL 单元测试实战指南:基于 cmocka 与 wrap 的隔离测试体系(test/unit) OpenSSL 作为一个承载 TLS 与密码学核心逻辑
密码学网络安全通信rust-by-example 单元测试实战:从 `[test]` 到 `cargo test` 的完整指南
rust by example 单元测试实战:从 test 到 cargo test 的完整指南 单元测试是 Rust 保证代码正确性的第一道防线。本篇基于 r
文档教程Umbraco Backoffice 前端测试体系实战指南:从 @web/test-runner 单元测试到 Playwright E2E 的完整测试栈
Umbraco Backoffice 前端测试体系实战指南:从 @web/test runner 单元测试到 Playwright E2E 的完整测试栈 本文是
CMS后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考