EDK II 单元测试框架权威指南:UnitTestFrameworkPkg 的 Framework 与 GoogleTest 双轨实践

原创2026-10-03 09:07:191,879 阅读
文章标签:固件嵌入式驱动开发

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 对照学习资源

若想直观对比两套框架的风格差异,仓库内提供了现成的对照样例:

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<...>/ 对应目录。仓库实例:

6.3 声明(.h)

头文件结构要点:

  1. 标准 #ifndef 保护;
  2. 必须包含 GoogleTestLib.h 与 FunctionMockLib.h;
  3. 其他 EDK II 相关 C 头文件必须用 extern "C" {} 包裹,否则会导致链接错误;
  4. 必须 #include 被 mock 的接口本身——这样编译期可校验 mock 与设计函数的签名完全一致;
  5. 声明一个与头文件同名的 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 因而断言了五个输入参数的精确值——任一不匹配即测试失败:

  1. VariableName 等于 EFI_CUSTOM_MODE_NAME 字符串(Char16StrEq);
  2. VendorGuid 等于 gEfiCustomModeEnableGuid(BufferEq,长度 sizeof(EFI_GUID));
  3. Attributes 等于 EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS;
  4. DataSize 等于 sizeof(SecureBootMode);
  5. 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。

登录后查看全文
edk2