
从AOSP源码目录里第一次翻到#include gtest/gtest.h的时候我其实是有点懵的。当时刚接手一个系统组件的维护代码还没看明白先看到一堆TEST(...)宏和EXPECT_EQ之类的调用第一反应是这玩意跑起来到底是个什么东西。后来因为一个偶现的内存问题领导直接甩了一句“先补个测试复现再谈修bug”我这才硬着头皮把GoogleTest在Android系统源码里的玩法完整摸了一遍。这一摸不要紧直接把我对“写测试”这件事的态度从“有空再写”变成了“不写测试根本不敢改代码”。这篇是Android系统源码系列的第19篇标题里写的是“GoogleTest技术全景”其实就是想把我从框架原理到AOSP集成再到实战踩坑的完整过程捋清楚。无论你是刚开始看系统源码、要给自己的native模块补测试还是单纯想搞明白C测试框架怎么选、怎么用这篇应该都能给你一个相对完整的参考。1. 为什么AOSP里到处是GoogleTest1.1 先搞明白它解决的是什么问题AOSP的代码量是千万行级别的牵一发动全身。改一个libbase的字符串函数可能影响上层几十个模块改一个HAL接口vendor那边可能直接编译失败。在这么大的工程里如果没有一套自动化的测试体系兜底任何一次重构都是在走钢丝。GoogleTest就是Google开源的那套C测试框架AOSP里native层的单元测试、组件测试、HAL测试大量跑在它上面。你在源码里搜gtest会看到libbase、libutils、art、hardware/interfaces下面的VTS测试、framework/native等等全都挂在GTest上。它和Java层的JUnit正好对应一个管native世界一个管Java世界。你可能要问为什么不直接用assert或者自己写个打印函数原因很简单GoogleTest提供了断言、测试夹具、参数化测试、死亡测试、mock支持这些工程化能力。你自己写一堆if (result ! expected) { printf(...); exit(1); }跑起来也能看个大概但人家已经把失败信息、测试统计、过滤运行、结果输出全都帮你规整好了你还自己发明轮子干嘛。1.2 它和JUnit、Catch2、Boost.Test的定位差异聊到C测试框架很多人会拿Catch2、Boost.Test来对比。我个人的选择逻辑是这样如果是在通用C项目里你喜欢BDD风格的SECTION那Catch2确实很香如果要依赖极少、编出来的二进制小到飞起Doctest也值得试试。但只要你人在AOSP里干活GoogleTest就是唯一合理的选择。框架断言风格参数化Mock支持AOSP集成适合场景GoogleTestTEST/EXPECT/ASSERTTEST_Pgmock原生支持Android系统、大型C工程Catch2SECTION/REQUIRE较差需要外部mock库手动配置通用C工程、小团队Boost.TestBOOST_AUTO_TEST支持较弱手动配置已经重度依赖Boost的工程DoctestCHECK一般无手动配置快速搭建、编译时间敏感的工程这个表格不是要分个高下而是想说清楚一个点Android系统源码之所以选GoogleTest核心原因是它和AOSP构建系统深度绑定。你写一个cc_test模块Android.bp里声明一下gtest依赖构建系统自动帮你链接、注册、产出可执行文件atest一下就能跑整个链路是通的。你用Catch2自己弄先不说集成成本光是处理各种隐式依赖就够你喝一壶的。1.3 系统组件测试的独特之处AOSP里的测试和普通应用层测试有个很大的不同它既要测“业务逻辑对不对”又要测“平台约定守没守住”。比如你写一个Binder服务的代理端测试重点不只是返回值对不对还要关注对端死掉时的表现、parcel参数校验失败时的行为。这些东西用GoogleTest的死亡测试和多线程配合比手写一堆try-catch直观得多。我见过很多刚开始看系统源码的开发者拿到一个模块先盯着一堆.cpp文件发呆其实更高效的入口是先看配套的*_test.cpp。测试代码就是模块的“活文档”——它告诉你这个类的正常路径长什么样、边界条件在哪、出错时应该抛什么。从这个角度说GoogleTest在AOSP里的角色不只是质量保障工具还是源码阅读的导航仪。2. GoogleTest核心机制逐个拆解2.1 TEST宏与断言体系一张表说清楚GTest最基础的单位是TEST(test_suite_name, test_name)一个用例就是这两个名字拼出来的完整路径。宏展开之后其实是一个普通的C函数里面写你的测试逻辑断言失败时框架会自动记录位置和失败原因。断言分两大家族EXPECT_*和ASSERT_*。区别就一句话EXPECT失败后继续跑ASSERT失败直接终止当前用例。拿C的异常来类比EXPECT相当于捕获异常后继续执行ASSERT相当于直接return。写系统级测试时一定要想清楚哪种失败模式是你想要的。比如你要检查一个初始化操作返回成功才能继续做后续步骤那就该用ASSERT_TRUE(init_result)否则后面跟着的检查全都是在空指针上跳舞。断言方式失败行为适用场景EXPECT_EQ(a, b)记录失败继续执行普通结果校验多条错误可以并报ASSERT_EQ(a, b)记录失败终止当前用例前置条件校验后续步骤依赖本次结果EXPECT_THROW(stmt, ExType)记录失败继续执行验证函数是否抛出预期异常ASSERT_DEATH(stmt, regex)记录失败终止验证程序是否按预期崩溃实际写代码的时候我习惯在同一个用例里关键的前置条件用ASSERT后面的结果校验用EXPECT。这样一次跑下来能看到尽量多的失败信息而不是第一个断言挂掉就全剧终。2.2 TestFixture把公共准备抽出来当你发现好几个用例都要做同样的初始化动作——比如创建同一个类的实例、准备同一份测试数据、启动某个模拟服务——那就该上TestFixture了。写法很简单继承::testing::Test在SetUp()里写公共准备工作在TearDown()里做清理然后用TEST_F代替TEST。class MyComponentTest : public ::testing::Test { protected: void SetUp() override { component_ std::make_uniqueMyComponent(); component_-Init(); } void TearDown() override { component_-Shutdown(); } std::unique_ptrMyComponent component_; }; TEST_F(MyComponentTest, InitThenProcess) { ASSERT_NE(component_, nullptr); EXPECT_TRUE(component_-Process(42)); }有个细节容易踩坑SetUp和TearDown不是构造/析构不能假定它们在RAII上有同样的异常安全语义。如果SetUp里申请了资源但后续初始化失败GTest还是会调用TearDown的所以清理动作放TearDown是保险的。在AOSP环境里经常会在SetUp里创建Looper、注册Binder服务或者准备一个带临时文件的目录这些资源如果忘了在TearDown里释放用例之间会互相污染表现就是“单个用例跑没问题一跑全套就随机挂”。这类问题我后面还会专门说。2.3 参数化测试一套代码遍历多种输入系统组件经常要验证“同一套逻辑在不同参数下都成立”。GoogleTest的参数化测试就是干这个用的。核心是TEST_P加上INSTANTIATE_TEST_SUITE_P把一组参数喂给同一个测试逻辑。class ParserParamTest : public ::testing::TestWithParamstd::string { }; TEST_P(ParserParamTest, HandlesValidInput) { const std::string input GetParam(); auto result ParseInput(input); EXPECT_TRUE(result.ok()); } INSTANTIATE_TEST_SUITE_P( ValidInputs, ParserParamTest, ::testing::Values(normal, with_spaces, tab\tseparated, unicode中文) );参数化测试的好处不只是少写几个用例函数更重要的是当后续新增一种输入格式时你只需要往Values列表里加一个元素新用例就自动长出来了。在维护HAL接口测试的时候经常需要遍历多个设备版本、多个数据格式用参数化可以极大降低拷贝粘贴带来的维护成本。要提醒一句给参数值起个可读的打印名是有价值的。GTest支持INSTANTIATE_TEST_SUITE_P的第四个参数传入一个打印函数把参数值转成可读字符串不然你看到失败用例的名字可能是一长串数字排查时很痛苦。2.4 死亡测试专门验证“该挂的时候就要挂”系统级代码里有很多路径是专门处理错误和异常状态的比如参数校验失败要abort、遇到非法状态要LOG(FATAL)。这种“期望崩溃”的行为如果直接在测试里调用测试进程本身也跟着挂了根本没法收集后续结果。GoogleTest提供了死亡测试机制来解决这个问题。ASSERT_DEATH(statement, regex)会启动一个子进程来执行statement然后检查子进程是否按预期退出以及输出是否匹配regex。注意死亡测试是有开销的每次都要fork一个子进程所以别把它放在高频循环里。另一个坑是某些平台或构建配置下堆栈展开和信号处理的差异会导致死亡测试不稳定。在Android平台上由于bionic的某些实现细节死亡测试有时候需要额外设置::testing::FLAGS_gtest_death_test_style threadsafe;否则可能出现偶发失败。我自己的经验是死亡测试非常适合验证“前置条件严重不满足时必须FATAL”这类行为但别滥用。能通过返回错误码处理的问题就不要设计成崩溃——这是产品代码的设计问题不是测试代码能兜住的。3. 把GoogleTest集成进AOSP构建系统3.1 Android.bp里的cc_test模块AOSP的构建系统用的是Soong模块定义写在Android.bp里。要编译一个基于GoogleTest的测试核心是声明一个cc_test模块并链接libgtest。下面是一个我实际用过的简化配置cc_test { name: my_component_test, srcs: [ my_component_test.cpp, my_component.cpp, ], local_include_dirs: [include], shared_libs: [ libbase, liblog, libgtest, ], static_libs: [ libgmock, ], cflags: [ -Wall, -Werror, ], }注意两点。第一libgtest在大多数Android版本里作为共享库提供但也有直接cc_test默认携带的情况。最简单的办法是打开一个系统里现成的测试模块抄它的Android.bp结构。第二如果你在测试代码里用了gmock来mock依赖对象记得在static_libs里显式加libgmock编译期不会报错但链接期会给你一串“undefined reference to testing::internal::...”的提示属于经典新手友好报错。3.2 atest与本地运行测试模块声明好之后运行测试有两条路径。第一条是atest这是AOSP专门用于跑测试的工具atest my_component_test它会自动完成编译、部署、运行、汇总结果的全流程。对于host侧测试也就是在开发机上直接跑的测试atest --host可以跳过连接设备的过程速度快很多。第二条是手动编译后直接执行二进制。在源码根目录执行source build/envsetup.sh lunch 你的目标 m my_component_test $ANDROID_PRODUCT_OUT/data/nativetest/my_component_test/my_component_test其实编译完成后可执行文件会被推到$ANDROID_PRODUCT_OUT/data/nativetest/模块名/下面也可以先adb sync data或直接adb push过去再跑。写测试和调试测试的前期我强烈建议先在host侧跑等逻辑稳定再转到target侧验证能节省大量往返刷机时间。3.3 从单测到系统级测试链路单测只是第一关。AOSP里的GoogleTest还可以被包装成更上层的测试用例进入自动化测试系统。比如VTS测试就是用GTest编写然后通过Trade Federation框架跑在设备上。这意味着你的测试代码很可能不只在本机运行还会进入持续集成系统每天跑在几十台设备上。因此测试代码的健壮性和资源清理非常关键——你写的一个全局静态对象在一台每天跑满8小时的测试机上可能触发各种意想不到的时序问题。在Android 10往后的版本里新的native测试也开始支持通过AndroidTest.xml描述测试配置把GTest用例结果自动上报到测试基础设施的指标系统。这一步不太容易在本地完全模拟但只要你的测试本身是stable的集成走上层框架只是配置问题不会反过来制造测试失败。4. 实战给一个native模块写全套测试4.1 选定测试对象与拆解测试点我拿一个实际经历来说。当时要维护的是一个负责解析配置字符串、输出结构化配置项的native模块核心接口类似下面这样class ConfigParser { public: explicit ConfigParser(const std::string raw); bool Parse(); const std::string GetValue(const std::string key) const; size_t GetSize() const; };面对这样一个类我给自己列了个测试清单正常输入各种配置项的解析格式不完整/多空格/重复key的边界场景空串和超长串的极端输入以及遍历接口在无数据时的行为。这个清单其实对应的是这个类会被调用的所有真实场景。写测试时最忌讳的就是对着实现代码写测试那叫“测试实现”不是“测试行为”。你应该问自己这个类对外承诺了什么行为把这些行为一条条列出来每个行为就是一个测试点。4.2 完整测试代码与逐步解析下面是我当时写的测试代码骨架#include gtest/gtest.h #include config_parser.h class ConfigParserTest : public ::testing::Test { protected: void SetUp() override { parser_ nullptr; } void TearDown() override { parser_ nullptr; } std::unique_ptrConfigParser parser_; }; TEST_F(ConfigParserTest, ParsesSingleKeyValue) { parser_ std::make_uniqueConfigParser(keyvalue); ASSERT_TRUE(parser_-Parse()); EXPECT_EQ(parser_-GetValue(key), value); EXPECT_EQ(parser_-GetSize(), 1u); } TEST_F(ConfigParserTest, ParsesMultipleEntries) { parser_ std::make_uniqueConfigParser(a1\nb2\nc3); ASSERT_TRUE(parser_-Parse()); EXPECT_EQ(parser_-GetValue(a), 1); EXPECT_EQ(parser_-GetValue(c), 3); } TEST_F(ConfigParserTest, HandlesBlankLinesGracefully) { parser_ std::make_uniqueConfigParser(a1\n\n\nb2); ASSERT_TRUE(parser_-Parse()); EXPECT_EQ(parser_-GetSize(), 2u); } TEST_F(ConfigParserTest, RejectsMissingEquals) { parser_ std::make_uniqueConfigParser(novalue); EXPECT_FALSE(parser_-Parse()); } TEST_F(ConfigParserTest, EmptyInputParsesToEmptyConfig) { parser_ std::make_uniqueConfigParser(); ASSERT_TRUE(parser_-Parse()); EXPECT_EQ(parser_-GetSize(), 0u); EXPECT_EQ(parser_-GetValue(anything), ); }几个细节值得展开。第一SetUp里只把parser_置空真正的创建动作放在每个用例里这是为了让用例之间的数据完全独立。第二ASSERT_TRUE(parser_-Parse())用的是ASSERT因为Parse失败后继续取GetValue完全是没意义的行为及时止损能避免后续产生误导性的失败信息。第三我在空串用例里特意验证了“取一个不存在的key”的表现这种边界最容易被开发者忽略。4.3 用gmock隔离依赖别让外部环境毁掉你的用例真实系统模块很少像上面这么简单。更多时候你的被测对象会依赖一个Binder接口、一个文件句柄、甚至一个网络会话。这时候就要用mock把外部依赖替换掉。gmock的基本用法是定义一个继承自接口的mock类用MOCK_METHOD声明要mock的方法然后在测试里设置期望。class IDevicePort { public: virtual ~IDevicePort() default; virtual bool Open() 0; virtual int Read(uint8_t* buf, size_t len) 0; }; class MockDevicePort : public IDevicePort { public: MOCK_METHOD(bool, Open, (), (override)); MOCK_METHOD(int, Read, (uint8_t* buf, size_t len), (override)); }; TEST(PortManagerTest, OpenFailurePropagates) { MockDevicePort mock; EXPECT_CALL(mock, Open()).WillOnce(::testing::Return(false)); PortManager manager(mock); EXPECT_FALSE(manager.Start()); }写mock测试最常见的坑是忘记设置期望的返回值gmock会在该调用发生的时刻直接给一个默认值这种隐式的默认行为容易让测试在你不注意的地方悄悄“假通过”。另外一个实践是mock只mock接口不要mock具体类。你要mock一个具体类说明这个类的接口设计违反了依赖倒置原则测试代码其实是在替你暴露设计问题。4.4 测试隔离为什么单个用例绿整套就红这是我从写系统测试第一天就被反复教育的事情。GoogleTest跑完一个用例后并不会帮你在内存层面“恢复出厂设置”。如果你在用例A里创建了一个全局单例并修改了它的状态用例B再跑的时候用的就是被污染的状态。AOSP里这种问题格外严重因为很多老模块就是爱用全局变量。应对办法有三个层次。第一优先用局部变量和依赖注入把状态都收敛到被测对象内部。第二确实需要全局状态时在SetUp和TearDown里显式保存/恢复现场。第三给测试进程加一个干净的启动环境——很多GTest用例可以在host上通过--gtest_repeat10之类的参数反复跑如果你的用例跑10次就挂了大概率就是全局状态污染。注意--gtest_shuffle和--gtest_repeat这两个参数强烈建议在提交测试代码前跑一遍。它们能帮你暴露用例之间的隐藏依赖。如果随机顺序跑挂了说明你的测试用例不是独立的这是比被测代码的bug更严重的隐患。5. 高频问题排查实录与避坑心得5.1 链接错误gtest_main到底要不要写新手写GTest时最典型的链接错误是undefined reference to main紧接着就是“你得自己写个main函数”。事实上GoogleTest提供了两组库libgtest和libgtest_main。如果你链接了libgtest_main它会提供一个默认的main函数负责初始化GTest并运行所有用例。在AOSP里很多cc_test模块并不会显式链接libgtest_main而是自己在测试文件里加一个简单的mainint main(int argc, char** argv) { ::testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }如果发现自己的测试模块只报了链接错误先看一眼别的模块是不是有这个main函数。还有一种情况是你链接了libgtest_main同时又自己在文件里写了main这时会报重复定义。这个问题的本质是链接库的符号冲突排查时先用nm查看导出符号会很有帮助别盲猜。5.2 测试跑起来找不到动态库在target侧跑native测试经常遇到的报错是dlopen failed: library libxxx.so not found。原因通常是测试模块没有把依赖的.so推到设备上或者依赖了某个生产模块但构建系统没有自动安装。解决办法是回看Android.bp确认shared_libs里把所有依赖都声明齐全。如果依赖的是系统里已有的库大多数情况下m之后会自动adb sync带上但如果你改了某个shared_libs没加进去只把测试二进制推到设备上运行时就必然崩给你看。5.3 断言失败信息不直观的排查技巧EXPECT_EQ失败时GTest会打印“期望值和实际值”但有时候两个值都是长字符串或者结构体指针打印出来的东西基本没法看。我的经验是用SCOPED_TRACE来给特定的循环或上下文追加标记信息。比如在参数化测试的循环里想精确知道是哪一组参数挂了for (const auto item : items) { SCOPED_TRACE(item key item.key); EXPECT_EQ(parser_-GetValue(item.key), item.expected); }当断言失败时SCOPED_TRACE里的内容会作为诊断信息一并打印你一眼就能定位到具体item。这个工具可比自己到处加printf再重新编译高效多了。5.4 偶发失败从玄学到定位的系统方法偶发失败是测试体系里最让人头疼的问题。我的排错顺序是这样的先用--gtest_repeat100 --gtest_break_on_failure复现这两个参数叠加能在第一次失败时停下帮你快速抓到崩溃现场。如果复现不出来检查用例之间是否共享了静态数据、临时文件、环境变量这仨是系统级测试偶发失败的三座大山。再不行就把怀疑对象放到--gtest_filter的独立进程里跑确认是否是跨用例影响。最后才去怀疑被测代码本身的并发问题。说实话大部分偶发失败都是测试用例自身不干净导致的先把自家地扫干净再去找别人的问题。5.5 一张问题速查表症状可能原因快速解决undefined reference to main缺少gtest_main或自定义main加main函数或链接libgtest_maindlopen找不到soAndroid.bp缺少依赖声明补全shared_libs并sync跑全套挂、单跑不挂用例间共享状态清理全局变量、检查临时文件冲突死亡测试偶发失败平台对fork支持差异设置FLAGS_gtest_death_test_stylethreadsafe随机顺序跑挂用例执行顺序产生隐式依赖用SetUp/TearDown保证状态独立5.6 写测试的几个心得我在AOSP里摸索这套东西最大的体会是写测试不是为了好看而是为了让代码改起来不慌。一个组件如果没有测试傍身你重构它的内部实现时心里是没底的不知道哪个依赖它的模块会突然冒出一串编译错误或者行为异常。有了测试你可以放心地把内部重写一遍只要测试还是绿的你就能以很高的置信度说“行为没变”。另外写GoogleTest用例和写普通代码一样也要注意可读性。给用例起名字时别用Test1、Test2这种要把行为和预期结果写进名字里比如ParsesSingleKeyValue、HandlesBlankLinesGracefully。别人跑挂了看一眼用例名就知道是什么场景省去翻代码定位的时间。最后分享一个控制测试节奏的小技巧在AOSP里跑测试时先用--gtest_filter锁定单个用例比如--gtest_filterConfigParserTest.*确认逻辑没问题后再跑整个模块。这样每一个失败点都能快速定位不会陷入几十个测试全都报红的泥潭。这个习惯让我在调试系统组件时节省了大量时间也推荐给你试试。