ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android NDK 原生代码单元测试实践:基于 googletest 与 junit-gtest 的设备端测试指南(ndk-samples unit-test 深度解析)

Android NDK 原生代码单元测试实践:基于 googletest 与 junit-gtest 的设备端测试指南(ndk-samples unit-test 深度解析) 示例工程移动开发【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址https://gitcode.com/gh_mirrors/nd/ndk-samples点击查看免费下载在 Android 开发中C/C 原生代码NDK的正确性往往依赖人工验证或集成测试兜底而官方推荐的 googletest 框架在移动设备上的落地方式一度缺乏开箱即用的示例。本指南以 ndk-samples 仓库中的 unit-test 示例为蓝本完整讲解如何用 googletest 为 NDK 原生代码编写单元测试并通过 junit-gtest 桥接层让测试在手机或模拟器上以 Android Instrumented Test 的形式运行。读完本文你将掌握从 CMake 配置、Gradle 依赖声明到 Kotlin 测试壳类编写的完整链路并能读懂失败断言在设备端的输出格式。一、示例整体结构一个被测库 测试库 桥接壳的三层模型unit-test 示例虽然业务逻辑极简一个两数相加函数却完整呈现了 NDK 单元测试的标准工程骨架。从仓库目录结构看它由三部分组成unit-test/app/src/main/cpp/ # 原生代码被测库与测试库 unit-test/app/src/main/java/ # 应用主代码JNI 入口与界面 unit-test/app/src/androidTest/ # 设备端测试壳JUnit 桥接被测库unittest由 adder.cpp 与 native-lib.cpp 编译而成通过 JNI 向 Kotlin 层暴露add方法测试库app_tests仅包含 adder_test.cpp链接 googletest 与 junit-gtest专门用于承载原生测试用例测试壳NativeTests位于 NativeTests.kt是一个空类通过注解把 googletest 用例翻译成 AndroidJUnitRunner 认识的 Instrumented Test。主界面 MainActivity.kt 在onCreate中调用add(1, 2)并显示结果native-lib.cpp 中的 JNI 函数把参数原样转发给add()。这个应用可用、测试也可用的设计正是关键被测代码同时服务于产品路径与测试路径杜绝了测试代码与应用代码分叉的问题。二、编写单元测试从被测函数到 googletest 用例2.1 被测代码保持头文件与实现分离被测库的核心是一个极简加法函数头文件 adder.h 只暴露声明#pragma once int add(int a, int b);实现 adder.cpp 同样直白#include adder.h int add(int a, int b) { return a b; }这里采用头文件 实现分离的经典布局让测试文件只依赖头文件即可编译也为后续用$TARGET_OBJECTS:adder复用目标文件埋下伏笔见第三节。2.2 测试用例gtest 宏的基本用法adder_test.cpp 是一个完整可编译的最小 gtest 用例#include adder.h #include gtest/gtest.h TEST(adder, adder) { EXPECT_EQ(3, add(1, 2)); }要点拆解TEST(TestSuiteName, TestName)是 googletest 定义测试用例的宏第一个参数为测试套件名第二个为用例名EXPECT_EQ(expected, actual)是非致命断言失败时记录错误但继续执行当前用例与ASSERT_EQ致命断言失败即中止形成互补。对单元测试而言EXPECT_*能一次跑出多个失败点更适合做行为校验测试文件通过#include gtest/gtest.h引入框架头文件而 googletest 头文件与库本身由 NDK 官方打包的 prefab 依赖提供详见第四节。三、构建配置CMake 侧如何织入测试库3.1 完整 CMakeLists 的职责划分unit-test/app/src/main/cpp/CMakeLists.txt 是理解整个构建体系的核心。它声明了三个构建目标cmake_minimum_required(VERSION 3.22.1) project(unittest) find_package(googletest REQUIRED CONFIG) find_package(junit-gtest REQUIRED CONFIG) # 被测对象库只编译不链接 add_library(adder OBJECT adder.cpp) # 应用共享库复用 adder 的目标文件 add_library(unittest SHARED $TARGET_OBJECTS:adder native-lib.cpp) # 测试共享库同样复用 adder 的目标文件并链接 gtest 与 junit-gtest add_library(app_tests SHARED adder_test.cpp) target_link_libraries(app_tests PRIVATE $TARGET_OBJECTS:adder googletest::gtest junit-gtest::junit-gtest )3.2 三个值得细读的设计点add_library(adder OBJECT ...)对象库adder.cpp被编译成目标文件而非独立库随后通过生成器表达式$TARGET_OBJECTS:adder同时注入unittest与app_tests两个库。这意味着同一份被测源码既进入 APK 内的应用库也进入测试库避免了两份编译产物行为不一致的隐患也无需为测试单独复制源码。find_package(... REQUIRED CONFIG)googletest 与 junit-gtest 都通过 prefab 机制以 CMake 包形式提供CONFIG模式从依赖的包目录加载*Config.cmake找到后即可使用googletest::gtest、junit-gtest::junit-gtest这两个 imported target 完成链接。app_tests是 SHARED 而非可执行文件在 Android 上原生测试不能以独立可执行文件直接运行缺少合适的进程宿主因此必须编译为.so并打入测试 APK再由 JVM 侧的 runner 加载执行——这正是 junit-gtest 桥接层存在的根本原因。3.3 为什么native-lib.cpp不进入测试库注意app_tests只链接了adder_test.cpp和adder对象库而native-lib.cppJNI 胶水属于应用库。从源码结构看这是刻意为之单元测试应聚焦纯逻辑函数不依赖 JNIEnv、jobject 等运行时环境把 JNI 层从测试范围中剥离让用例在设备上以最轻量方式运行。四、Gradle 配置prefab 开关、依赖版本与测试库隔离4.1 开启 prefab 并声明依赖unit-test/app/build.gradle 中完成了两件事。首先因为 googletest 通过 prefab 分发必须在android块中开启该特性buildFeatures { viewBinding true prefab true }其次在dependencies中引入桥接库与测试框架示例通过 gradle/libs.versions.toml 统一管理版本dependencies { implementation libs.androidx.junit.gtest // androidx.test.ext:junit-gtest:1.0.0-alpha02 implementation libs.googletest // com.android.ndk.thirdparty:googletest:1.11.0-beta-1 ... }对应版本目录gradle/libs.versions.toml中的实际定义为androidxJUnitGTest 1.0.0-alpha02 googletest 1.11.0-beta-1 androidx-junit-gtest { group androidx.test.ext, name junit-gtest, version.ref androidxJUnitGTest } googletest { group com.android.ndk.thirdparty, name googletest, version.ref googletest }两点说明com.android.ndk.thirdparty:googletest是NDK 官方预构建的 prefab 包内含各 ABI 的libgtest及 CMake 配置文件因此无需在源码树中手动 vendoring googletest版本以当前仓库实际内容为准README 中写的1.0.0-alpha01在版本目录中已升级为1.0.0-alpha02googletest 为1.11.0-beta-1。4.2 测试库隔离packagingOptions的关键作用app/build.gradle中有一处容易被忽略但至关重要的配置packagingOptions { jniLibs { // Gradle has no way of knowing which of the libraries in our // CMakeLists.txt are for the app and which are for tests, so we // have to tell it which libraries are test libraries. Without // this, the test libraries will end up packaged in the real API // and not just the test APK. testOnly [**/libapp_tests.so] } }注释说得很清楚Gradle 无法从 CMakeLists 推断哪些库服务于应用、哪些服务于测试。若不加testOnlylibapp_tests.so会被打进正式 APK白白增大安装包体积。把**/libapp_tests.so标记为testOnly后它只进入测试 APK。如果你复制本项目务必把这里改成自己测试库的名字如libmytests.so。4.3 Instrumentation RunnerdefaultConfig中还声明了testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner这是运行设备端测试所必需的 runner它与NativeTests壳类配合完成 googletest 用例的调度与结果回传。五、Kotlin 桥接壳把 gtest 用例暴露给 AndroidJUnitRunner原生测试库编译完成后还需要一个翻译层让 JVM 侧的测试框架发现并执行它。NativeTests.kt 全文如下package com.example.unittest import androidx.test.ext.junitgtest.GtestRunner import androidx.test.ext.junitgtest.TargetLibrary import org.junit.runner.RunWith RunWith(GtestRunner::class) TargetLibrary(libraryName app_tests) class NativeTests逐行解读RunWith(GtestRunner::class)把标准 JUnit4 runner 替换为 androidx 提供的GtestRunner其职责是加载指定原生库、枚举其中的 googletest 用例并逐一执行再把通过/失败结果映射回 JUnit 的测试报告TargetLibrary(libraryName app_tests)指定要加载的测试共享库名。注意不包含lib前缀与.so后缀——GtestRunner会自动补全为libapp_tests.soclass NativeTests类体为空即可所有测试逻辑都在 C 侧。若后续新增多个测试库可对应增加多个带不同TargetLibrary的壳类。这个壳类必须放在androidTest源集unit-test/app/src/androidTest/java/com/example/unittest/NativeTests.kt因为它属于 Instrumented Test需要运行在设备上。六、在设备上运行测试与解读失败输出6.1 运行方式在 Android Studio 中直接对NativeTests类或包右键选择Run即可在连接的手机或模拟器上执行这与 Android 官方 Instrumented Test 的运行方式完全一致也可用命令行./gradlew :unit-test:app:connectedDebugAndroidTest6.2 失败断言的输出格式示例特意演示了故意破坏测试的情形——把断言改成EXPECT_EQ(4, add(1, 2))EXPECT_EQ(4, add(1, 2));运行后会在测试报告中看到类似如下输出java.lang.AssertionError: /path/to/ndk-samples/unit-test/app/src/main/cpp/adder_test.cpp:6 Expected equality of these values: 4 add(1,2) Which is: 3这段输出信息量很大值得逐字段解读java.lang.AssertionErrorgoogletest 的失败通过 junit-gtest 转换为 JVM 侧的断言异常从而被 JUnit 报告捕获源码位置adder_test.cpp:6精确指向触发断言的测试文件与行号此处即EXPECT_EQ所在行便于快速定位Expected equality of these values:是EXPECT_EQ的标准失败模板随后分别列出期望值与实际表达式Which is: 3给出add(1,2)的实际计算结果即失败根因是真实返回值 3 与期望值 4 不符。这种期望值 / 实际表达式 / 实际值 / 源文件行号四位一体的报错结构是 gtest 优于裸printf调试的核心价值所在。修复断言为EXPECT_EQ(3, add(1, 2))后测试即恢复绿色。七、运行效果与工程启示上图是示例在设备上的实际运行效果界面通过 JNI 调用add(1, 2)展示 1 2 3。它同时印证了本示例同一份add()既服务应用、又服务测试的设计——应用的显示逻辑与测试用例共享同一实现任何一方改动都能立即在另一方暴露问题。从工程实践角度看该示例给出了三条可复用的方法论用对象库 $TARGET_OBJECTS:复用被测代码从构建层面杜绝测试副本与产品代码分叉用 prefab 引入预构建的 googletest免去源码级集成 gtest 的维护成本用testOnly隔离测试库确保测试产物不污染正式 APK。八、延伸阅读本示例完整源码unit-test/app/src/main/cpp/CMakeLists.txt、unit-test/app/src/androidTest/java/com/example/unittest/NativeTests.kt、unit-test/app/build.gradle版本与依赖统一管理gradle/libs.versions.toml仓库内其他 NDK 实战示例的架构说明ARCHITECTURE.md补充说明本文涉及的 googletest 与 junit-gtest 版本以当前仓库 gradle/libs.versions.toml 为准googletest1.11.0-beta-1、junit-gtest1.0.0-alpha02。alpha/beta版本表明 API 仍在演进生产项目接入前建议关注其正式版本发布情况。赞分享示例工程移动开发【免费下载链接】ndk-samplesAndroid NDK samples with Android Studio项目地址https://gitcode.com/gh_mirrors/nd/ndk-samples点击查看免费下载相关推荐在真机上跑C单元测试ndk-samples unit-test样本googletest原生测试深度解析在真机上跑C单元测试ndk samples unit test样本googletest原生测试深度解析 Android 原生代码怎么写单元测试 ndk示例工程移动开发Android NDK Samples深度解析入门JNI与基础开发实践Android NDK Samples深度解析入门JNI与基础开发实践 Android NDK Samples项目是Google官方维护的综合性示例代码库专示例工程移动开发OpenSSL 单元测试实战指南基于 cmocka 与 --wrap 的隔离测试体系test/unitOpenSSL 单元测试实战指南基于 cmocka 与 wrap 的隔离测试体系test/unit OpenSSL 作为一个承载 TLS 与密码学核心逻辑密码学网络安全通信上一篇StaffML 语料库北极星 v3如何从四个原则性约束推导出 12,000–14,000 道 ML 系统面试题下一篇Ente Photos 画廊模式Gallery Mode完全指南无账号的端侧本地相册体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表