news 2026/10/14 3:15:44

testing-samples 系列实战:BasicUnitAndroidTest 设备端单元测试(Instrumented Unit Test)完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
testing-samples 系列实战:BasicUnitAndroidTest 设备端单元测试(Instrumented Unit Test)完整指南
  • 示例工程

【免费下载链接】testing-samples

A collection of samples demonstrating different frameworks and techniques for automated testing

项目地址:https://gitcode.com/gh_mirrors/te/testing-samples
点击查看免费下载

本篇技术指南围绕 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.bat

README 中建议按以下顺序阅读这三个类:

  • 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 中运行的完整六步流程:

  1. 下载项目代码,推荐使用git clone方式获取 testing-samples 仓库;
  2. 在 Android Studio 中选择File|Open...,指向项目的./build.gradle文件打开工程;
  3. 阅读相关源码:LogHistory.java是被测类(实现Parcelable),LogHistoryAndroidUnitTest是 Android 单元测试,MainActivity.java展示Parcelable的实际用法——但再次强调,测试运行过程中不会在设备或模拟器上显示任何 Activity;
  4. 创建自定义运行器的测试配置,运行器指定为androidx.test.runner.AndroidJUnitRunner:
    • 打开Run菜单 |Edit Configurations
    • 新增一个Android Tests配置
    • 选择一个模块(本示例中即app)
    • 添加Specific instrumentation runner:androidx.test.runner.AndroidJUnitRunner
  5. 连接真机或启动模拟器;
  6. 运行新建的测试配置。

配置完成后,单元测试将自动执行,无需任何手动干预。需要说明的是,第 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); } }

这条测试链路完整模拟了"序列化 → 反序列化 → 数据比对"的闭环,其中三个细节值得注意:

  1. Parcel.obtain()与setDataPosition(0):obtain()从全局池中取出一块可复用的Parcel缓冲区;写入完成后必须调用setDataPosition(0)把读取游标重置回起点,否则读取会从缓冲区末尾开始,直接导致数据错乱。这正是 README 所说"写入与读取并不 trivial"的直观体现;
  2. 走CREATOR.createFromParcel而非直接调用私有构造函数:反序列化走的是与 Android 系统一致的公开入口,验证的是真实的Parcelable.Creator契约;
  3. 断言采用 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

项目地址:https://gitcode.com/gh_mirrors/te/testing-samples
点击查看免费下载
上一篇:opencodex 桌面端上下文窗口与推理强度控制:Cycle 2 A-门审计合成实战解析
下一篇:Convex Backend 自托管到自有服务器:Docker 部署、域名路由与 CLI 接入完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:15:03

MySQL零基础入门:从安装建库到增删改查全流程教程

1. MySQL到底是个啥&#xff0c;为什么零基础也要学它这一篇我们聊聊 MySQL。不管你之前有没有接触过数据库&#xff0c;只要你会用电脑打字&#xff0c;跟着这份超详细的教程&#xff0c;就能把 MySQL 装起来、跑起来&#xff0c;然后亲手写下第一条 SQL。我见过不少零基础学编…

作者头像 李华
网站建设 2026/10/14 3:14:49

ORA-12505故障精解:Oracle监听器服务注册与连接描述符匹配原理

1. 问题本质与典型场景还原“ORA-12505, TNS:listener does not currently know of SID given in connect descriptor”——这行报错&#xff0c;我在过去十年带过的二十多个Oracle数据库项目里&#xff0c;几乎每三个月就要亲手处理一次。它不是那种高深莫测的内核级错误&…

作者头像 李华
网站建设 2026/10/14 3:14:10

Spring AOP 从入门到原理:一文搞懂面向切面编程与动态代理

一. 为什么需要AOP&#xff1f;首先&#xff0c;我们先来用一个例子来说明 AOP 存在的意义。就简单地以统计各个接口方法的执行耗时为例&#xff0c;根据我们之前学习的知识来说&#xff0c;我们需要在每个业务方法前后分别统计开始时间、结束时间并计算出所使用的时间&#xf…

作者头像 李华
网站建设 2026/10/14 3:14:02

AI预览版与前沿隐藏能力:普通从业者如何用出专家级效果

前段时间&#xff0c;一位常年泡在 AI 前沿一线的研究者在公开场合说了句话&#xff0c;大意是&#xff1a;外界看到的 AI 能力&#xff0c;其实只是一段“预览版”的演示&#xff1b;真正的前沿系统&#xff0c;早就在处理那些连专业人才都要耗上数年才能完成的活了。这句话让…

作者头像 李华
网站建设 2026/10/14 3:13:21

C++冒泡排序从入门到优化:原理、实现与性能对比

1. 为什么冒泡排序值得你花时间搞懂很多人第一次接触排序算法&#xff0c;学的就是冒泡排序。但也正因为它是"第一课"&#xff0c;不少人学完就扔&#xff0c;觉得这玩意儿除了教学演示&#xff0c;实际开发中根本用不上。我一开始也这么想&#xff0c;直到后来带新人…

作者头像 李华