EDK II 单元测试框架权威指南:UnitTestFrameworkPkg 的 Framework 与 GoogleTest 双轨实践
EDK II 单元测试框架权威指南:UnitTestFrameworkPkg 的 Framework 与 GoogleTest 双轨实践
导读
本文全面解读 EDK II 开源固件项目中负责单元测试的 UnitTestFrameworkPkg 包,围绕其官方 ReadMe.md 展开:先介绍两套测试框架(Framework 与 GoogleTest)的定位与选型,再逐步讲解测试工程的 INF/DSC/代码配置、断言与日志用法、gMock/FunctionMockLib 打桩技术、构建运行与结果解析。读者读完可掌握在 UEFI Shell、Host 乃至 PEI/DXE/SMM 环境中编写、构建、运行和调试 EDK II 单元测试的完整实战方案。
1. 包定位:为 EDK II 而生的单元测试框架
UnitTestFrameworkPkg 的目标是:让开发者专注于"写测试逻辑",而把错误日志、结果格式化、上下文持久化、测试调度等公共职责交给框架。它支持两类执行环境:
- UEFI Shell 等固件内目标环境(Target-Based):测试作为 PEI、DXE、SMM 模块或 UEFI Shell 应用运行;
- 主机环境(Host-Based):测试编译为
HOST_APPLICATION,在开发机上直接运行,无需真实固件目标。
框架对低层单元测试和系统级测试都适用,且容易接入 CI 自动化。官方将测试源码集中在包的 Test/UnitTest 与 Test/GoogleTest 两个目录下,并在 UnitTestFrameworkPkg.dsc、UnitTestFrameworkPkgCommon.dsc.inc 等文件中组织构建。
2. 两套框架:Framework 与 GoogleTest
2.1 Framework
第一套框架名为 Framework,以一组 EDK II 库实现,同时支持主机端与目标端测试,两者共享同一套源码风格、宏和 API——在部分场景下,同一份测试用例源码可以分别编译为 Host 测试与目标测试。主机端测试若需要打桩(mock),可借助包内以子模块方式引入的 cmocka。
2.2 GoogleTest
第二套框架是 GoogleTest,仅用于主机端单元测试(EDK II 组件的目标端测试不支持 GoogleTest)。它的主要优势:
- 注册测试套件/用例的开销小(自动注册);
- 生态工具丰富,如 VS Code 的 C++ TestMate 扩展;
- 支持值参数化、类型参数化、Typed Tests、超时控制、子集执行、Address Sanitizer 等 Framework 所不具备的能力。
主机端测试若需要打桩,GoogleTest 自带 gMock;但由于 gMock 不直接支持对 C 风格自由函数的打桩,包内专门提供了 FunctionMockLib(一组包装 MOCK_METHOD 的宏)来补齐该能力。
注意:本文会引用若干外部项目(cmocka、GoogleTest、subhook)的文档,但以仓库内实现为准。
2.3 特性对比速查表
| 特性 | Framework | GoogleTest |
|---|---|---|
| 主机端单元测试 | YES | YES |
| 目标端单元测试 | YES | NO |
| 测试源码语言 | C | C++ |
| 注册测试套件 | 手动 | 自动 |
| 注册测试用例 | 手动 | 自动 |
| 期望断言测试 | YES | YES |
| Setup/Teardown 钩子 | YES | YES |
| 值参数化测试 | NO | YES |
| Typed 测试 | NO | YES |
| 类型参数化测试 | NO | YES |
| 超时支持 | NO | YES |
| 打桩支持 | Cmocka | gMock |
| JUNIT XML 报告 | YES | YES |
| 执行测试子集 | NO | YES |
| VS Code 扩展 | NO | YES |
| Address Sanitizer | Cmocka | YES |
选型建议:官方建议,凡是能用主机端测试覆盖的组件,优先使用 GoogleTest。
2.4 对照学习资源
若想直观对比两套框架的风格差异,仓库内提供了现成的对照样例:
- BaseSafeIntLib 单元测试同时有 Framework 与 GoogleTest 两个版本:MdePkg/Test/UnitTest/Library/BaseSafeIntLib 与 MdePkg/Test/GoogleTest/Library/BaseSafeIntLib;
- SecureBootVariableLib 的测试同时使用 cmocka 与 gMock 两种打桩风格:SecurityPkg/Library/SecureBootVariableLib/UnitTest 与 SecurityPkg/Library/SecureBootVariableLib/GoogleTest。
3. Framework 核心库拆解
3.1 UnitTestLib —— 框架本体
核心库,定义 Framework 对象:可注册任意数量的测试套件与用例,并驱动测试执行。测试条件检查与错误报告通过一组断言宏完成,状态与错误信息会写入测试上下文。框架还支持在测试执行期间记录日志字符串,供测试报告阶段使用。该库的公共 API 声明于 MdePkg/Include/Library/UnitTestLib.h,实现位于 UnitTestFrameworkPkg/Library/UnitTestLib。
3.2 UnitTestPersistenceLib —— 上下文持久化
负责将测试上下文保存/恢复到存储介质,使测试可以在"退出进程后恢复状态"的场景中保持连续性——这对"测试运行中途需要系统重启"的场景至关重要。包内提供两种实例:空实现 UnitTestPersistenceLibNull 与简单文件系统实现 UnitTestPersistenceLibSimpleFileSystem。
3.3 UnitTestResultReportLib —— 报告格式化
在测试运行结束时负责生成报告,是常见的定制点,可让测试输出适配其他测试基础设施。包内提供两个简单实例:UnitTestResultReportLibConOut 与 UnitTestResultReportLibDebugLib,均以纯文本形式输出到控制台。实现位于 UnitTestFrameworkPkg/Library/UnitTestResultReportLib。
3.4 示例工程
Framework 示例位于 Test/UnitTest/Sample/SampleUnitTest,提供 PEI、SMM、DXE、UEFI App 与 HOST_APPLICATION 五种"口味"(对应多个 .inf:SampleUnitTestPei.inf、SampleUnitTestSmm.inf、SampleUnitTestDxe.inf、SampleUnitTestUefiShell.inf、SampleUnitTestHost.inf),共享同一份测试逻辑源码 SampleUnitTest.c。
4. Framework 实战:从零搭建一个测试
本节以 SampleUnitTestUefiShell 应用为主线,走完 INF → DSC → 代码 → 运行 的全流程。
4.1 INF 配置
测试模块需要引入 UnitTestLib 库。其接口头文件位于 MdePkg(即 MdePkg/Include/Library/UnitTestLib.h),因此 INF 中无需依赖其他包,只要 DSC 能定位到要用的库实现即可。参考 SampleUnitTestUefiShell.inf:
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
BaseLib
DebugLib
UnitTestLib
PrintLib
提示:若希望测试被 Test Runner 插件自动拾取,模块
BASE_NAME必须包含Test字样:
[Defines]
BASE_NAME = SampleUnitTestUefiShell
4.2 DSC 配置
在 DSC 的 [Components] 中加入该 INF,使其参与构建(见 UnitTestFrameworkPkg.dsc):
[Components]
UnitTestFrameworkPkg/Test/UnitTest/Sample/SampleUnitTest/SampleUnitTestUefiShell.inf
同时,按测试类型在 DSC 顶部包含对应的框架配置文件:
!include UnitTestFrameworkPkg/UnitTestFrameworkPkgTarget.dsc.inc
若测试构建还依赖特定的库,可在 [Components] 的 <LibraryClasses> 子节中覆写映射——这正是控制"链接设计实现还是 mock 实现"的关键位置。以下示例来自 SecurityPkgHostTest.dsc,测试 SecureBootVariableLib 时以多个 mock 库替换真实实现:
[Components]
SecurityPkg/Library/SecureBootVariableLib/UnitTest/SecureBootVariableLibUnitTest.inf {
<LibraryClasses>
SecureBootVariableLib|SecurityPkg/Library/SecureBootVariableLib/SecureBootVariableLib.inf
UefiRuntimeServicesTableLib|SecurityPkg/Library/SecureBootVariableLib/UnitTest/MockUefiRuntimeServicesTableLib.inf
PlatformPKProtectionLib|SecurityPkg/Library/SecureBootVariableLib/UnitTest/MockPlatformPKProtectionLib.inf
UefiLib|SecurityPkg/Library/SecureBootVariableLib/UnitTest/MockUefiLib.inf
}
4.3 代码骨架与 Framework 配置
测试代码首先要包含:
#include <Library/UnitTestLib.h>
框架的一切围绕 Framework 对象展开。初始化使用 InitUnitTestFramework,其参数为长名、短名与版本三个 CHAR8 字符串。长名与版本仅用于展示,比较自由;短名会用于命名缓存文件与测试结果文件,应取有意义的名称。这些字符串会被内部拷贝,因此栈上字符串或字面量均可。示例:
DEBUG(( DEBUG_INFO, "%a v%a\n", UNIT_TEST_APP_NAME, UNIT_TEST_APP_VERSION ));
//
// Start setting up the test framework for running the tests.
//
Status = InitUnitTestFramework( &Framework, UNIT_TEST_APP_NAME, gEfiCallerBaseName, UNIT_TEST_APP_VERSION );
创建测试套件使用 CreateUnitTestSuite:入参为 Framework 句柄、套件标题、包名,以及可选的 setup/teardown 函数指针。套件标题用于展示;包名用于 xUnit 类报告,采用 . 分隔的层级格式。setup/teardown 分别在整个套件开始前/结束后各调用一次(而非每个用例前),不需要时传 NULL,其原型为 UNIT_TEST_SUITE_SETUP 与 UNIT_TEST_SUITE_TEARDOWN:
//
// Populate the SimpleMathTests Unit Test Suite.
//
Status = CreateUnitTestSuite( &SimpleMathTests, Fw, "Simple Math Tests", "Sample.Math", NULL, NULL );
规则:至少需要一个测试套件,即使只是"兜底"性质的。
注册用例使用 AddTestCase:入参为套件句柄、描述字符串、类名、用例函数指针,以及可选的 prerequisite、cleanup 与 context:
AddTestCase( SimpleMathTests, "Adding 1 to 1 should produce 2", "Addition", OnePlusOneShouldEqualTwo, NULL, NULL, NULL );
- 描述/类名的用法与套件的标题/包名一致(展示 vs xUnit 解析);
- prerequisite(原型
UNIT_TEST_PREREQUISITE)在用例执行前立即调用,若返回错误,用例将不运行并被记录为UNIT_TEST_ERROR_PREREQUISITE_NOT_MET; - cleanup(原型
UNIT_TEST_CLEANUP)在用例执行后立即调用,用于复位用例可能改动的全局状态;若 prerequisite 失败,cleanup 同样被跳过; - context 完全由测试自定义,会原样传给用例,典型用途是"同一用例以不同输入数据复用",无需时传
NULL。
全部注册完成后执行:
//
// Execute the tests.
//
Status = RunAllTestSuites( Framework );
4.4 编写第一个用例
用例函数原型必须匹配 UNIT_TEST_FUNCTION:
UNIT_TEST_STATUS
EFIAPI
OnePlusOneShouldEqualTwo (
IN UNIT_TEST_FRAMEWORK_HANDLE Framework,
IN UNIT_TEST_CONTEXT Context
)
{
UINTN A, B, C;
A = 1;
B = 1;
C = A + B;
UT_ASSERT_EQUAL(C, 2);
return UNIT_TEST_PASSED;
} // OnePlusOneShouldEqualTwo()
- 返回
UNIT_TEST_STATUS,结果会被框架记录并在所有套件结束后统一报告; - 断言宏失败时会立即从用例返回
UNIT_TEST_ERROR_TEST_FAILED并记录错误字符串(注意:这种提前返回可能带来内存泄漏隐患); - 推荐选择与判定标准最贴合的断言宏(日志信息随宏细化而更详细),拿不准时用
UT_ASSERT_TRUE/UT_ASSERT_FALSE;全部通过后返回UNIT_TEST_PASSED。
4.5 断言宏与日志速查
框架提供丰富的断言宏(定义于 MdePkg/Include/Library/UnitTestLib.h):
| 宏 | 语义 |
|---|---|
UT_ASSERT_TRUE(Expression) |
表达式为真则继续,否则失败返回 |
UT_ASSERT_FALSE(Expression) |
表达式为假则继续,否则失败返回 |
UT_ASSERT_EQUAL(ValueA, ValueB) |
两个 64 位简单值相等(UINT64 比较) |
UT_ASSERT_NOT_EQUAL(ValueA, ValueB) |
两个简单值不相等 |
UT_ASSERT_MEM_EQUAL(BufferA, BufferB, Length) |
两块内存按字节相等 |
UT_ASSERT_NOT_EFI_ERROR(Status) |
!EFI_ERROR(Status) 成立 |
UT_ASSERT_STATUS_EQUAL(Status, Expected) |
两个 EFI_STATUS 相等 |
UT_ASSERT_NOT_NULL(Pointer) |
指针非空 |
UT_EXPECT_ASSERT_FAILURE(FunctionCall, Status) |
期望 FunctionCall 触发 ASSERT()(基于 SetJump/LongJump 安全返回点) |
断言失败时框架会记录:失败表达式原文、所在函数、源文件与行号。测试状态取值(见 MdePkg/Include/Library/UnitTestLib.h):UNIT_TEST_PASSED(0)、UNIT_TEST_ERROR_PREREQUISITE_NOT_MET(1)、UNIT_TEST_ERROR_TEST_FAILED(2)、UNIT_TEST_ERROR_CLEANUP_FAILED(3),另有 SKIPPED/RUNNING/PENDING 等运行态。日志级别通过 PcdUnitTestLogLevel 位定义控制:ERROR/WARN/INFO/VERBOSE(BIT0~BIT3)。
从源码结构看(SampleUnitTest.c),prerequisite 与 cleanup 的典型用法是:用例执行前校验全局指针已为 NULL,用例执行后将全局指针复位——适合需要操纵共享状态的测试。
4.6 进阶:cmocka
编写主机端进阶测试时,可以引入 cmocka.h,直接使用 Cmocka 的全部能力。cmocka 以子模块形式包含在 UnitTestFrameworkPkg/Library/CmockaLib/cmocka 中。其官方文档站点为 https://api.cmocka.org/。
5. GoogleTest 核心库拆解
5.1 GoogleTestLib —— 核心封装
GoogleTestLib 是对 GoogleTest/gMock 头文件与源码的封装库,因此 GoogleTest 与 gMock 的标准用法完全适用。它额外补充了 EDK II 场景所需的原始类型支持,主要自定义集中在 UnitTestFrameworkPkg/Include/Library/GoogleTestLib.h:
自定义 Actions(用于 EXPECT_CALL() 中给输出参数赋值):
| Action 名 | 类似 gMock 通用 Action | 用法 |
|---|---|---|
SetArgBuffer() |
SetArgPointee() |
用数据设置缓冲区输出参数(如 UINT8*、VOID*、结构体指针等) |
自定义 Matchers(用于 EXPECT_CALL()、EXPECT_THAT() 等任意需要比较两缓冲区的场合):
| Matcher 名 | 类似 gMock 通用 Matcher | 用法 |
|---|---|---|
BufferEq() |
Pointee(Eq()) |
比较两个缓冲区指针类型(如 UINT8*、VOID*、结构体指针等) |
Char16StrEq() |
Pointee(Eq()) |
比较两个 CHAR16* 字符串 |
从源码实现看,BufferEq 逐字节比较并在首个不匹配字节处输出十六进制详情,Char16StrEq 逐 CHAR16 字符比较并给出 0xXXXX 与可打印字符提示(UnitTestFrameworkPkg/Include/Library/GoogleTestLib.h)。SetArgBuffer 基于 ACTION_TEMPLATE 模板实现,对输出参数执行 memcpy。
5.2 FunctionMockLib —— 自由函数打桩的关键
gMock 的 MOCK_METHOD 只支持类成员方法,无法直接打桩 C 自由函数。FunctionMockLib 通过宏包装 MOCK_METHOD 解决了这一问题。其公开宏定义与实现位于 UnitTestFrameworkPkg/Include/Library/FunctionMockLib.h,共六个公开宏:
接口宏(创建承载 mock 函数的接口并接入 gMock 框架):
| 宏 | 作用 |
|---|---|
MOCK_INTERFACE_DECLARATION(MOCK) |
在 .h 文件中声明接口 |
MOCK_INTERFACE_DEFINITION(MOCK) |
在 .cpp 文件中定义接口 |
函数宏(创建 mock 函数):
| 宏 | 参数 | 用途 |
|---|---|---|
MOCK_FUNCTION_DECLARATION(RET_TYPE, FUNC, ARGS) |
返回类型、函数名、参数列表 | 声明外部函数 mock(.h) |
MOCK_FUNCTION_DEFINITION(MOCK, FUNC, NUM_ARGS, CALL_TYPE) |
接口名、函数名、参数个数、调用约定 | 定义外部函数 mock(.cpp) |
MOCK_FUNCTION_INTERNAL_DECLARATION(RET_TYPE, FUNC, ARGS) |
同上 | 声明内部函数 mock(.h) |
MOCK_FUNCTION_INTERNAL_DEFINITION(MOCK, FUNC, NUM_ARGS, CALL_TYPE) |
同上 | 定义内部函数 mock(.cpp) |
5.3 SubhookLib —— 内部打桩的底层支撑
内部 mock 宏(MOCK_FUNCTION_INTERNAL_*)用于打桩"已在本模块内编译"的函数,需要运行时替换能力,因此依赖 SubhookLib(对 subhook 库的封装)。其原理是:把 mock 函数的第二定义编译进测试程序,测试执行时通过 subhook 将设计函数钩子替换为 mock 函数。使用 mock 宏本身无需了解 SubhookLib——它被完全封装在 FunctionMockLib 内部,此处仅为完整性说明。实现位于 UnitTestFrameworkPkg/Library/SubhookLib/subhook。
6. FunctionMockLib 打桩全流程
6.1 先判断:外部 vs 内部
创建 mock 前必须先明确被 mock 函数相对被测模块是外部还是内部,这决定使用哪套宏:
- 外部 mock:mock 的是被测模块之外的库/全局服务/协议函数,采用链接期替换——mock 函数定义随测试程序链接,替代设计函数定义;
- 内部 mock:mock 的是被测模块内部已定义的函数,采用运行期替换——mock 函数以稍改的名称编译进测试程序,执行时钩子替换设计函数。
| 场景 | 接口宏 | 函数宏 |
|---|---|---|
| 外部 mock | MOCK_INTERFACE_DECLARATION + MOCK_INTERFACE_DEFINITION |
MOCK_FUNCTION_DECLARATION + MOCK_FUNCTION_DEFINITION |
| 内部 mock | MOCK_INTERFACE_DECLARATION + MOCK_INTERFACE_DEFINITION |
MOCK_FUNCTION_INTERNAL_DECLARATION + MOCK_FUNCTION_INTERNAL_DEFINITION |
6.2 文件命名与放置规则
头文件命名取决于被 mock 的接口类型:
| 接口类型 | 头文件名 | 源文件名 |
|---|---|---|
| 库 | Mock<LibraryName>Lib.h |
Mock<LibraryName>Lib.cpp |
| 全局表(如 gRT、gBS) | Mock<GlobalTableLibraryName>Lib.h |
Mock<GlobalTableLibraryName>Lib.cpp |
| 协议 | Mock<ProtocolName>Protocol.h |
Mock<ProtocolName>Protocol.cpp |
放置位置:内部 mock 的头/源文件放在构建测试程序的 GoogleTest 目录;外部 mock 放在被 mock 接口所在包的 Test 目录下——头文件统一放 <PackageName>/Test/Mock/Include/GoogleTest/ 下的 Library/(库、全局表)或 Protocol/(协议),源文件放 <PackageName>/Test/Mock/Library/GoogleTest/Mock<...>/ 对应目录。仓库实例:
- 头文件:MdePkg/Test/Mock/Include/GoogleTest/Library/MockUefiLib.h
- 源文件:MdePkg/Test/Mock/Library/GoogleTest/MockUefiLib/MockUefiLib.cpp
6.3 声明(.h)
头文件结构要点:
- 标准
#ifndef保护; - 必须包含
GoogleTestLib.h与FunctionMockLib.h; - 其他 EDK II 相关 C 头文件必须用
extern "C" {}包裹,否则会导致链接错误; - 必须
#include被 mock 的接口本身——这样编译期可校验 mock 与设计函数的签名完全一致; - 声明一个与头文件同名的
struct,内含MOCK_INTERFACE_DECLARATION及每个函数的MOCK_FUNCTION_DECLARATION(或内部版)。
以 MockUefiLib.h 为例(节选):
#ifndef MOCK_UEFI_LIB_H_
#define MOCK_UEFI_LIB_H_
#include <Library/GoogleTestLib.h>
#include <Library/FunctionMockLib.h>
extern "C" {
#include <Uefi.h>
#include <Library/UefiLib.h>
}
struct MockUefiLib {
MOCK_INTERFACE_DECLARATION (MockUefiLib);
MOCK_FUNCTION_DECLARATION (
EFI_STATUS,
GetVariable2,
(IN CONST CHAR16 *Name,
IN CONST EFI_GUID *Guid,
OUT VOID **Value,
OUT UINTN *Size OPTIONAL)
);
MOCK_FUNCTION_DECLARATION (
EFI_STATUS,
GetEfiGlobalVariable2,
(IN CONST CHAR16 *Name,
OUT VOID **Value,
OUT UINTN *Size OPTIONAL)
);
};
#endif
命名调整:库函数在 mock 中保持原名;为避免符号冲突,全局表与协议的函数名需调整——例如 gRT->GetVariable 的 mock 名为 gRT_GetVariable,UsbIoProtocol->UsbPortReset 的 mock 名为 UsbIoProtocol_UsbPortReset。
gMock 的两条限制(同样约束 FunctionMockLib):不支持超过 15 个参数的函数,不支持可变参数(variadic)函数。
6.4 定义(.cpp)
源文件开头 #include 声明头文件,随后:
#include <GoogleTest/Library/MockUefiLib.h>
MOCK_INTERFACE_DEFINITION(MockUefiLib);
MOCK_FUNCTION_DEFINITION(MockUefiLib, GetVariable2, 4, EFIAPI);
MOCK_FUNCTION_DEFINITION(MockUefiLib, GetEfiGlobalVariable2, 3, EFIAPI);
要点:
- 若被 mock 函数未声明调用约定,
MOCK_FUNCTION_DEFINITION最后一个参数可留空:MOCK_FUNCTION_DEFINITION(MockUefiLib, GetVariable2, 4, ); MOCK_FUNCTION_DEFINITION中的函数名必须与头文件MOCK_FUNCTION_DECLARATION一致;- 若 mock 的是全局表或协议,还需在源文件中以
static结构体定义函数指针表,将各 mock 函数定义赋给对应表项,并把该结构体地址赋给全局表/协议指针——该指针必须用extern "C" {}包裹(C 链接),否则链接错误。以MockUefiRuntimeServicesTableLib.cpp为例(节选):
#include <GoogleTest/Library/MockUefiRuntimeServicesTableLib.h>
MOCK_INTERFACE_DEFINITION(MockUefiRuntimeServicesTableLib);
MOCK_FUNCTION_DEFINITION(MockUefiRuntimeServicesTableLib, gRT_GetVariable, 5, EFIAPI);
MOCK_FUNCTION_DEFINITION(MockUefiRuntimeServicesTableLib, gRT_SetVariable, 5, EFIAPI);
static EFI_RUNTIME_SERVICES localRt = {
{0}, // EFI_TABLE_HEADER
NULL, // EFI_GET_TIME
NULL, // EFI_SET_TIME
NULL, // EFI_GET_WAKEUP_TIME
NULL, // EFI_SET_WAKEUP_TIME
NULL, // EFI_SET_VIRTUAL_ADDRESS_MAP
NULL, // EFI_CONVERT_POINTER
gRT_GetVariable, // EFI_GET_VARIABLE
NULL, // EFI_GET_NEXT_VARIABLE_NAME
gRT_SetVariable, // EFI_SET_VARIABLE
NULL, // EFI_GET_NEXT_HIGH_MONO_COUNT
NULL, // EFI_RESET_SYSTEM
NULL, // EFI_UPDATE_CAPSULE
NULL, // EFI_QUERY_CAPSULE_CAPABILITIES
NULL, // EFI_QUERY_VARIABLE_INFO
};
extern "C" {
EFI_RUNTIME_SERVICES* gRT = &localRt;
}
6.5 构建:外部 mock 的独立 INF
外部 mock 函数用独立 INF 构建,INF 与被 mock 的源文件同目录、同名(扩展名 .inf)。关键字段:
BASE_NAME与文件名(去扩展名)相同;MODULE_TYPE为HOST_APPLICATION;LIBRARY_CLASS为BASE_NAME去掉Mock前缀(例如MockUefiLib→UefiLib);[Sources]只含该 mock 源文件;[Packages]至少包含UnitTestFrameworkPkg.dec;[LibraryClasses]含GoogleTestLib;[BuildOptions]需为 MSFT 构建追加/EHsc编译标志以启用 C++ 异常处理。
示例 MockUefiLib.inf:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MockUefiLib
FILE_GUID = 47211F7A-6D90-4DFB-BDF9-610B69197C2E
MODULE_TYPE = HOST_APPLICATION
VERSION_STRING = 1.0
LIBRARY_CLASS = UefiLib
#
# The following information is for reference only and not required by the build tools.
#
# VALID_ARCHITECTURES = IA32 X64
#
[Sources]
MockUefiLib.cpp
[Packages]
MdePkg/MdePkg.dec
UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec
[LibraryClasses]
GoogleTestLib
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /EHsc
为确保 mock 始终可构建(即使暂无测试使用),需把该 INF 加入所在包的 HostTest DSC 的 <a href="https://link.gitcode.com/i/bec3b321f46070a26489c3e4fba738d4" target="_blank">Components],例如 [MdePkg/Test/MdePkgHostTest.dsc:
[Components]
MdePkg/Test/Mock/Library/GoogleTest/MockUefiLib/MockUefiLib.inf
附加要求:若某包此前没有任何外部 mock,需要在包的 DEC 文件的
[Includes]中加入Test/Mock/Include,否则测试文件无法定位 mock 头文件。
6.6 构建:内部 mock 直接编入测试
内部 mock 无需独立 INF,直接作为源文件加进已有的 GoogleTest INF:在 [Sources] 加入 mock 定义文件、[Packages] 加入 UnitTestFrameworkPkg.dec、[LibraryClasses] 加入 GoogleTestLib 与 SubhookLib:
[Defines]
INF_VERSION = 0x00010017
BASE_NAME = MyModuleGoogleTest
FILE_GUID = 814B09B9-2D51-4786-8A77-2E10CD1C55F3
VERSION_STRING = 1.0
MODULE_TYPE = HOST_APPLICATION
#
# The following information is for reference only and not required by the build tools.
#
# VALID_ARCHITECTURES = IA32 X64
#
[Sources]
MyModuleGoogleTest.cpp
MockMyModuleInternalFunctions.cpp
[Packages]
MdePkg/MdePkg.dec
UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec
[LibraryClasses]
GoogleTestLib
SubhookLib
7. GoogleTest 实战:从零搭建一个测试
本节以 SampleGoogleTestHost 为主线,mock 相关示例参考 SecurityPkg 中的 SecureBootVariableLibGoogleTest。
7.1 INF 配置
引入 GoogleTestLib,其接口头文件就在 UnitTestFrameworkPkg 内(UnitTestFrameworkPkg/Include/Library/GoogleTestLib.h),无需依赖其他包。参考 SampleGoogleTestHost.inf:
[Packages]
MdePkg/MdePkg.dec
UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec
[LibraryClasses]
GoogleTestLib
BaseLib
DebugLib
同样,BASE_NAME 需含 Test 以便被 Test Runner 插件自动拾取:
[Defines]
BASE_NAME = SampleGoogleTestHost
7.2 DSC 配置
在 <a href="https://link.gitcode.com/i/3c0bfbf593b77db3714896867cf02709" target="_blank">Components] 加入 INF(见 [UnitTestFrameworkPkg/Test/UnitTestFrameworkPkgHostTest.dsc):
[Components]
UnitTestFrameworkPkg/Test/GoogleTest/Sample/SampleGoogleTest/SampleGoogleTestHost.inf
并包含主机端框架配置:
!include UnitTestFrameworkPkg/UnitTestFrameworkPkgHost.dsc.inc
重要:主机端测试的 DSC 不得包含
MdePkg/MdeLibs.dsc.inc这类面向固件构建的默认映射——其默认 defines 与库映射可能与主机端构建不兼容。主机测试 DSC 必须自行提供全部所需设置。
依赖库的覆写同样放在 <LibraryClasses> 子节,且正是在这里决定链接设计实现还是 mock 实现。SecurityPkgHostTest.dsc 的实例——测试 SecureBootVariableLib 设计实现,同时用 mock 版替换三个依赖库:
[Components]
SecurityPkg/Library/SecureBootVariableLib/GoogleTest/SecureBootVariableLibGoogleTest.inf {
<LibraryClasses>
SecureBootVariableLib|SecurityPkg/Library/SecureBootVariableLib/SecureBootVariableLib.inf
UefiRuntimeServicesTableLib|MdePkg/Test/Mock/Library/GoogleTest/MockUefiRuntimeServicesTableLib/MockUefiRuntimeServicesTableLib.inf
PlatformPKProtectionLib|SecurityPkg/Test/Mock/Library/GoogleTest/MockPlatformPKProtectionLib/MockPlatformPKProtectionLib.inf
UefiLib|MdePkg/Test/Mock/Library/GoogleTest/MockUefiLib/MockUefiLib.inf
}
7.3 代码骨架
GoogleTest 应用使用 C++,测试文件必须是 .cpp 扩展名。文件头部的包含方式:
#include <Library/GoogleTestLib.h>
extern "C" {
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
}
第一个 include 引入 GoogleTest 定义;其余 EDK II 相关 C 头文件必须用 extern "C" {} 包裹,否则链接失败。建议为 testing 命名空间加 using 声明(如 using namespace testing;),可大量减少 ::testing::Return、::testing::Test 这类冗长引用。若测试用到 mock 函数,还需包含对应 mock 头文件:
#include <Library/GoogleTestLib.h>
#include <GoogleTest/Library/MockUefiLib.h>
#include <GoogleTest/Library/MockUefiRuntimeServicesTableLib.h>
extern "C" {
#include <Uefi.h>
...
}
using namespace testing;
7.4 配置与入口
与 Framework 不同,GoogleTest 无需手动注册测试套件/用例——测试实现时就声明了套件名与用例名。唯一要求是提供 main() 初始化 GoogleTest 并调用 RUN_ALL_TESTS():
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
7.5 简单用例
来自 SampleGoogleTestHost:
TEST(SimpleMathTests, OnePlusOneShouldEqualTwo) {
UINTN A;
UINTN B;
UINTN C;
A = 1;
B = 1;
C = A + B;
ASSERT_EQ (C, 2);
}
要点:
TEST()的第一个参数是套件名、第二个是套件内用例名;- 断言失败立即返回并使
TEST失败;ASSERT_*失败立即返回,EXPECT_*失败则继续执行(两者最终都会判失败); - GoogleTest 用例没有返回值——只要没有触发断言/期望失败即为通过;
- 同样注意断言失败提前返回可能带来的内存泄漏隐患。
7.6 gMock 用例(含重构对比)
一个"直写"的 gMock 用例(来自 SecureBootVariableLibGoogleTest 的测试逻辑):验证当 gRT->SetVariable() 返回 EFI_INVALID_PARAMETER 时,SetSecureBootMode() 也返回该状态:
TEST(SetSecureBootModeTest, SetVarError) {
MockUefiRuntimeServicesTableLib RtServicesMock;
UINT8 SecureBootMode;
EFI_STATUS Status;
// Any random magic number can be used for these tests
SecureBootMode = 0xAB;
EXPECT_CALL(RtServicesMock, gRT_SetVariable)
.WillOnce(Return(EFI_INVALID_PARAMETER));
Status = SetSecureBootMode(SecureBootMode);
EXPECT_EQ(Status, EFI_INVALID_PARAMETER);
}
关键理解:
- 必须实例化接口对象(此处
RtServicesMock),mock 函数才存在、可用; EXPECT_CALL第一个参数是已实例化的接口对象,第二个参数是接口内 mock 函数名;WillOnce(Return(...))声明该函数本次测试内被调用一次并返回指定值;- 随后调用被测代码
SetSecureBootMode(),其内部调用gRT->SetVariable()时命中期望返回,最终用EXPECT_EQ校验返回值。
实际测试文件中该用例会进一步重构为 test fixture 形式,把 mock 实例与公共变量上移到基类:
class SetSecureBootModeTest : public Test {
protected:
MockUefiRuntimeServicesTableLib RtServicesMock;
UINT8 SecureBootMode;
EFI_STATUS Status;
void SetUp() override {
// Any random magic number can be used for these tests
SecureBootMode = 0xAB;
}
};
TEST_F(SetSecureBootModeTest, SetVarError) {
EXPECT_CALL(RtServicesMock, gRT_SetVariable)
.WillOnce(Return(EFI_INVALID_PARAMETER));
Status = SetSecureBootMode(SecureBootMode);
EXPECT_EQ(Status, EFI_INVALID_PARAMETER);
}
fixture 使所有 TEST_F(SetSecureBootModeTest, ...) 自动获得其成员变量,并在进入用例前自动执行 SetUp(),测试体得以聚焦被测行为。另一个基于同一 fixture 的用例还展示了参数匹配与自定义 matcher 的用法:
TEST_F(SetSecureBootModeTest, PropagateModeToSetVar) {
EXPECT_CALL(RtServicesMock,
gRT_SetVariable(
Char16StrEq(EFI_CUSTOM_MODE_NAME),
BufferEq(&gEfiCustomModeEnableGuid, sizeof(EFI_GUID)),
EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS,
sizeof(SecureBootMode),
BufferEq(&SecureBootMode, sizeof(SecureBootMode))))
.WillOnce(Return(EFI_SUCCESS));
Status = SetSecureBootMode(SecureBootMode);
EXPECT_EQ(Status, EFI_SUCCESS);
}
gRT_SetVariable() 的参数顺序(设计函数签名):VariableName、VendorGuid、Attributes、DataSize、Data。上述 EXPECT_CALL 因而断言了五个输入参数的精确值——任一不匹配即测试失败:
VariableName等于EFI_CUSTOM_MODE_NAME字符串(Char16StrEq);VendorGuid等于gEfiCustomModeEnableGuid(BufferEq,长度sizeof(EFI_GUID));Attributes等于EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS;DataSize等于sizeof(SecureBootMode);Data等于SecureBootMode的内容(BufferEq,长度sizeof(SecureBootMode))。
7.7 进阶资源
更高级的 GoogleTest 用法可参考 GoogleTest 官方用户指南;更多宿主样例见 UnitTestFrameworkPkg/Test/GoogleTest/Sample(含 SampleGoogleTest、SampleGoogleTestExpectFail、SampleGoogleTestGenerateException 等)。
8. 开发与调试技巧
8.1 单测迭代
使用 EDK2 Pytools CI 时,只要构建包含 NOOPT 目标,主机端单元测试就会被构建并运行。迭代单个测试时只构建该模块即可,例如只构建 MdePkg 的 SafeIntLib 主机测试:
stuart_ci_build -c .pytool/CISettings.py TOOL_CHAIN_TAG=VS2026 -p MdePkg -t NOOPT BUILDMODULE=MdePkg/Test/UnitTest/Library/BaseSafeIntLib/TestBaseSafeIntLib.inf
8.2 Hooking BaseLib
大多数打桩可由 UnitTestFrameworkPkg 的库完成,但 BaseLib 被框架自身消费,需要特殊手段替换。主机端测试消费一个带钩子表(hook table)的特殊 BaseLib 实现,可替换任意 BaseLib 函数;默认走底层实现,测试编写者只需为特定用例提供最小替换代码。钩子表接口定义于 MdePkg/Test/UnitTest/Include/Library/UnitTestHostBaseLib.h。
8.3 调试框架本身
框架内部(主要是 Cmocka)会设置自己的构建标志,导致部分符号不导出并捕获异常,较难单步调试。可给 Stuart 构建追加参数强制启用符号调试:
stuart_ci_build -c .pytool/CISettings.py TOOL_CHAIN_TAG=VS2026 -p MdePkg -t NOOPT BLD_*_UNIT_TESTING_DEBUG=TRUE
9. 构建与运行主机端测试
EDK2 CI 基础设施通过 Stuart 构建系统(EDK2-PyTools 发布)与 NOOPT 目标,可便捷地运行整棵树或指定包的全部主机端测试并聚合报告。以下以 Framework 为例,GoogleTest 测试的构建运行方式相同(仅文本输出与 JUNIT XML 输出略有差异)。
9.1 本地构建步骤
先升级 PyTools(建议在 Python venv 中执行):
python -m pip install --upgrade -r ./pip-requirements.txt
随后依次执行:
# Setup repo for building
stuart_setup -c ./.pytool/CISettings.py TOOL_CHAIN_TAG=VS2026
# Update all binary dependencies
stuart_update -c ./.pytool/CISettings.py TOOL_CHAIN_TAG=VS2026
# Build and run the tests
stuart_ci_build -c ./.pytool/CISettings.py TOOL_CHAIN_TAG=VS2026 -t NOOPT -p MdePkg
TOOL_CHAIN_TAG 可按环境换成 GCC 等;-p 用于限定包。
禁用 Address Sanitizer
默认所有主机端测试构建开启地址消毒器。开发/调试时可通过 DSC define 关闭:
stuart_ci_build -c ./.pytool/CISettings.py TOOL_CHAIN_TAG=VS2026 -t NOOPT -p MdePkg BLD_*_UNIT_TESTING_ADDRESS_SANITIZER_ENABLE=FALSE
9.2 结果解读
构建输出中失败会以 WARNING / ERROR 高亮,例如:
PROGRESS - Running Build NOOPT
PROGRESS - Running Post Build
SECTION - Run Host based Unit Tests
SUBSECTION - Testing for architecture: X64
WARNING - TestBaseSafeIntLibHost.exe Test Failed
WARNING - Test SafeInt8ToUint8 - UT_ASSERT_EQUAL(0x5b:5b, Result:5c)
c:\_uefi\edk2\MdePkg\Test\UnitTest\Library\BaseSafeIntLib\TestBaseSafeIntLib.c:35: error: Failure!
ERROR - Plugin Failed: Host-Based Unit Test Runner returned 1
CRITICAL - Post Build failed
ERROR - --->Test Failed: Host Unit Test Compiler Plugin NOOPT returned 1
ERROR - Overall Build Status: Error
测试失败时可手动运行可执行文件获取更多细节:
.\Build\MdePkg\HostTest\NOOPT_VS2026\X64\TestBaseSafeIntLibHost.exe
Int Safe Lib Unit Test Application v0.1
---------------------------------------------------------
------------ RUNNING ALL TEST SUITES --------------
---------------------------------------------------------
RUNNING TEST SUITE: Int Safe Conversions Test Suite
[==========] Running 71 test(s).
[ RUN ] Test SafeInt8ToUint8
[ ERROR ] --- UT_ASSERT_EQUAL(0x5b:5b, Result:5c)
[ LINE ] --- c:\_uefi\edk2\MdePkg\Test\UnitTest\Library\BaseSafeIntLib\TestBaseSafeIntLib.c:35: error: Failure!
[ FAILED ] Test SafeInt8ToUint8
[ RUN ] Test SafeInt8ToUint16
[ OK ] Test SafeInt8ToUint16
...
CI 实际运行产生的结构化结果位于:
Build/<Package>/HostTest/<Arch>/<TestName>.<TestSuiteName>.<Arch>.result.xml
示例内容:
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites>
<testsuite name="Int Safe Conversions Test Suite" time="0.000" tests="71" failures="1" errors="0" skipped="0" >
<testcase name="Test SafeInt8ToUint8" time="0.000" >
<failure><![CDATA[UT_ASSERT_EQUAL(0x5c:5c, Result:5b)
c:\_uefi\MdePkg\Test\UnitTest\Library\BaseSafeIntLib\TestBaseSafeIntLib.c:35: error: Failure!]]></failure>
</testcase>
<testcase name="Test SafeInt8ToUint16" time="0.000" >
</testcase>
...
9.3 手动运行可执行文件的环境变量
stuart_ci_build 会自行设置主机端测试所需环境变量;若从 shell 或 VS Code 扩展(如 C++ TestMate)手动运行,需手动配置。VS Code TestMate 的 JSON 配置示例:
"testMate.cpp.test.advancedExecutables": [
{
"pattern": "Build/**/*Test*",
"cwd": "${absDirpath}",
"env": {
"GTEST_CATCH_EXCEPTIONS": "0",
"ASAN_OPTIONS": "detect_leaks=0",
}
}
],
Windows:
set GTEST_CATCH_EXCEPTIONS=0
set ASAN_OPTIONS=detect_leaks=0
Linux:
export GTEST_CATCH_EXCEPTIONS=0
export ASAN_OPTIONS=detect_leaks=0
9.4 XML 报告模式
- Framework(基于 Cmocka)生成结构化 XML 需要:
CMOCKA_MESSAGE_OUTPUT=xml
CMOCKA_XML_FILE=<absolute or relative path to output file>
- GoogleTest 生成 XML 需要:
GTEST_OUTPUT=xml:<absolute or relative path to output file>
测试运行插件正是用该模式聚合结果,供 CI 状态报告在 Web 视图展示。
9.5 代码覆盖率
主机端单元测试会自动启用覆盖率数据。
- Windows:主要面向流水线构建,本地可用 OpenCppCoverage 解析为 cobertura xml:
Download and install https://github.com/OpenCppCoverage/OpenCppCoverage/releases
python -m pip install --upgrade -r ./pip-requirements.txt
stuart_ci_build -c .pytool/CISettings.py -t NOOPT TOOL_CHAIN_TAG=VS2026 -p MdeModulePkg
Open Build/coverage.xml
Visual Studio 查看方式:Tools -> OpenCppCoverage Settings 填入可执行文件路径,然后 Tools -> Run OpenCppCoverage。
- Linux:本地用 lcov 工具采集,再用 lcov_cobertura 解析为 cobertura xml:
sudo apt-get install -y lcov
python -m pip install --upgrade -r ./pip-requirements.txt
stuart_ci_build -c .pytool/CISettings.py -t NOOPT TOOL_CHAIN_TAG=GCC -p MdeModulePkg
Open Build/coverage.xml
VS Code 查看方式:安装 "Coverage Gutters" 插件,Ctrl + Shift + P 选择 Coverage Gutters: Display Coverage。
重要说明:以上功能在 Windows 与 Linux 均可用,但目前仅限 x64 架构。
10. Framework 已知限制
10.1 PEI、DXE、SMM
已提供这些环境的示例测试(见 UnitTestFrameworkPkg/Test/UnitTest/Sample/SampleUnitTest 中的 SampleUnitTestPei.inf、SampleUnitTestDxe.inf、SampleUnitTestSmm.inf),但仅做过初步构建验证。框架设计时已考虑启动阶段执行,但只有 UEFI Shell 与主机端测试经过充分评估;PEI/DXE/SMM 的完整支持仍在推进中,当前应视为 beta/暂存状态。
10.2 主机端与其他测试的差异
主机端框架内部由 Cmocka 驱动,能力上并不完全等同于目标端测试的超集。例如:目标端测试会把日志消息缓存并关联到运行中的用例,可随后由报告库保存;而主机端目前只能记录断言失败。此类不一致仍在打磨中。
11. 测试目录布局规范
为保持全仓库一致,官方对测试代码的位置有明确约定:
| 代码/测试类型 | 位置 |
|---|---|
| 面向库/协议/PPI/GUID 接口的主机端测试 | 测试与具体实现无关时,放在父包级:MdePkg/Test/UnitTest/[Library/Protocol/Ppi/Guid]/。实例:MdePkg/Test/UnitTest/Library/BaseSafeIntLib/TestBaseSafeIntLibHost.inf |
| 面向库/驱动(PEI/DXE/SMM)实现的主机端测试 | 放在实现目录自身的 UnitTest(或 GoogleTest)子目录。实例:MdeModulePkg/Universal/EsrtFmpDxe/UnitTest/、MdePkg/Library/BaseMemoryLib/UnitTest/、SecurityPkg/Library/SecureBootVariableLib/GoogleTest/ |
| 面向功能/特性的主机端测试 | 模块级功能测试放在包级 Test 目录下的 HostFuncTest 子目录(如 FmpDevicePkg/Test/HostFuncTest/FmpDeviceFramework);跨包特性由相关包 owner 决定 |
| 非主机端(PEI/DXE/SMM/Shell)功能测试 | 放在 *Pkg/Test/[Shell/Dxe/Smm/Pei]Test 目录;跨包特性由相关包 owner 决定 |
11.1 示例目录树
<PackageName>Pkg/
ComponentY/
ComponentY.inf
ComponentY.c
GoogleTest/
ComponentYHostGoogleTest.inf # Host-Based Test for Driver Module
ComponentYGoogleTest.cpp
UnitTest/
ComponentYHostUnitTest.inf # Host-Based Test for Driver Module
ComponentYUnitTest.c
Library/
GeneralPurposeLibBase/
...
GeneralPurposeLibSerial/
...
SpecificLibDxe/
SpecificLibDxe.c
SpecificLibDxe.inf
GoogleTest/ # Host-Based Test for Specific Library Implementation
SpecificLibDxeHostGoogleTest.cpp
SpecificLibDxeHostGoogleTest.inf
UnitTest/ # Host-Based Test for Specific Library Implementation
SpecificLibDxeHostUnitTest.c
SpecificLibDxeHostUnitTest.inf
Test/
<Package>HostTest.dsc # Host-Based Test Apps
GoogleTest/
InterfaceX
InterfaceXHostGoogleTest.inf
InterfaceXUnitTest.cpp
GeneralPurposeLib/
GeneralPurposeLibTest.cpp
GeneralPurposeLibHostUnitTest.inf
UnitTest/
InterfaceX
InterfaceXHostUnitTest.inf
InterfaceXPeiUnitTest.inf # PEIM Target-Based Test (if applicable)
InterfaceXDxeUnitTest.inf # DXE Target-Based Test (if applicable)
InterfaceXSmmUnitTest.inf # SMM Target-Based Test (if applicable)
InterfaceXShellUnitTest.inf # Shell App Target-Based Test (if applicable)
InterfaceXUnitTest.c
GeneralPurposeLib/
GeneralPurposeLibTest.c
GeneralPurposeLibHostUnitTest.inf
Mock/
Include/
GoogleTest/
Library/
MockGeneralPurposeLib.h
Library/
GoogleTest/
MockGeneralPurposeLib/
MockGeneralPurposeLib.cpp
MockGeneralPurposeLib.inf
<Package>Pkg.dsc # Standard Modules and any Target-Based Test Apps (including in Test/)
11.2 规划中的位置
- 主机端库实现(如
MemoryAllocationLibHost):应放在声明该库接口的包的*Pkg/HostLibrary目录,名称含Host; - 主机端 mock 与 stub:应放在
UefiTestFrameworkPkg/StubLibrary,库名含Mock或Stub。
注:以上为官方规划的"未来位置"(来自 ReadMe 的 Future Locations 章节),仓库中
UefiTestFrameworkPkg尚不存在,仅作布局参考。
12. 总结
UnitTestFrameworkPkg 为 EDK II 提供了双轨测试体系:Framework 以 C 语言与显式注册支持主机端 + 目标端(UEFI Shell/PEI/DXE/SMM),配合 cmocka 打桩;GoogleTest 以 C++ 与自动注册专攻主机端,配合 gMock 与 FunctionMockLib 打桩。两者共享相似的使用心智模型(INF/DSC 接入、断言宏、报告输出),并可通过 Stuart/NOOPT 一键构建运行。本文涉及的示例与对照工程均可直接在仓库中查看:Framework 样例在 UnitTestFrameworkPkg/Test/UnitTest/Sample,GoogleTest 样例在 UnitTestFrameworkPkg/Test/GoogleTest/Sample,mock 样例在 SecurityPkg/Library/SecureBootVariableLib/GoogleTest。