[Architecture] 给 CppUTest 注入灵魂:用 C++ 特性在嵌入式测试中实现“类 Spring AOP”切面

一、痛点:Java 工程师的“水土不服”

作为一名习惯了 Java 生态(JUnit / Spring)的开发者,在转入嵌入式 C 语言开发并使用 CppUTest 框架时,我不禁感到一丝“水土不服”。

在 JUnit 中,我们要控制测试的生命周期简直随心所欲:

  • @BeforeClass / @AfterClass: 全局初始化/销毁
  • @Before / @After: 每个测试前后执行
  • @Around: 环绕通知(最强大的切面)

然而,在 CppUTest 中,标准宏 TEST_GROUP 只给了我们两个钩子:

  • setup()
  • teardown()

这就好比开惯了全自动档的车,突然给了我一辆手动档的手扶拖拉机。如果我想做更细粒度的控制(比如:在 setup 之前做一些全局 Mock 的重置,或者在测试执行期间自动计算耗时),标准的 setup/teardown 显得捉襟见肘。

难道必须修改框架源码才能实现吗?
不。作为一名追求 “White-box” (白盒化) 的工程师,通过阅读 CppUTest 源码,我发现了一个优雅的扩展点。

二、白盒分析:发现“后门”

深入 CppUTest 的 UtestMacros.h,我发现了 TEST_GROUP 宏背后的秘密:

1
2
3
// 源码片段
#define TEST_GROUP(testGroup) \
TEST_GROUP_BASE(testGroup, Utest)

这意味着,我们平时写的 TEST_GROUP(LedDriver),本质上是定义了一个继承自 Utest 基类的 C++ 类。

Insight (洞察): 既然是 C++ 继承,那我们完全可以替换掉这个基类! 只要我们定义一个中间层基类,接管 setupteardown 的调用权,就能实现自定义的生命周期编排。这就是经典的 Template Method (模板方法) 设计模式。

此外,利用 C++ 的 RAII (资源获取即初始化) 特性,我们还可以轻松实现 Java 中复杂的 @Around 环绕通知。

三、解决方案:AOP-like 生命周期实现

这一方案不需要任何第三方库,完全利用 C++ 语言特性。

1. 全局生命周期 (Template Method 模式)

我们可以定义一个 MyTestLifecycle 类,继承自 Utest。在这个类中,我们“霸占”了标准的 setup/teardown,并将它们转化为调度器,去调用我们自定义的细分钩子。

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @description 自定义生命周期基类
* 实现了:init -> setup -> test -> teardown -> after_execute -> destroy
*/
class MyTestLifecycle : public Utest
{
public:
// --- 自定义钩子 (虚函数,供子类覆盖) ---
virtual void my_init() { }
virtual void before_execute() { }
virtual void after_execute() { }
virtual void my_destroy() { }

// --- 接管框架入口 ---
void setup() _override
{
my_init(); // 1. 自定义初始化
before_execute(); // 2. 自定义前置
// 注意:这里还可以插入通用的 Mock 初始化逻辑
}

// --- 接管框架出口 ---
void teardown() _override
{
after_execute(); // 3. 自定义后置
my_destroy(); // 4. 自定义销毁
}
};

2. 局部环绕 (RAII 模式)

C++ 的栈对象在作用域结束时会自动调用析构函数,这是实现 “Around” (环绕) 最完美的机制,甚至比 Java 的 try-finally 更优雅。

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @description 环绕切面:利用构造和析构实现
*/
class LogAspect
{
public:
LogAspect(const char* name) {
printf("\n>>> [Around Start] Entering %s\n", name);
}

~LogAspect() {
printf("\n<<< [Around End] Exiting scope\n");
}
};

四、实战演示

现在,我们将这两个模式应用到实际的 LedDriver 测试中。

关键点: 使用 TEST_GROUP_BASE 宏来指定我们的自定义基类。

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "CppUTest/TestHarness.h"
extern "C" {
#include "LedDriver.h"
}

// 使用 TEST_GROUP_BASE 继承我们的生命周期类
TEST_GROUP_BASE(LedDriver, MyTestLifecycle)
{
// 覆盖自定义钩子
void my_init() _override
{
printf("\n>>> 1. Custom Init\n");
// 比如:在此处初始化内存池
}

void after_execute() _override
{
printf("\n<<< 3. Custom After\n");
// 比如:在此处检查内存泄漏
}

// setup() 和 teardown() 已被基类接管,无需重复写
};

TEST(LedDriver, LedsOffAfterCreate)
{
printf("\n--- 2. Test Body ---\n");

// 【织入切面】
// 构造函数立即执行(前置通知)
LogAspect aspect("TestWithAround");

uint16_t virtualLeds;
LedDriver_Create(&virtualLeds);
LedDriver_TurnOn(1);
LONGS_EQUAL(0, virtualLeds); // 这里的断言即使失败,aspect 的析构依然会执行

// 函数结束,aspect 析构自动执行(后置通知)
}

运行结果

执行 make 后,控制台输出完美验证了我们的生命周期设计:

Plaintext

1
2
3
4
5
6
7
8
9
10
11
>>> 1. Custom Init

--- 2. Test Body ---

>>> [Around Start] Entering TestWithAround

<<< [Around End] Exiting scope

<<< 3. Custom After

OK (1 tests, 1 ran, 1 checks, 0 ignored, 0 filtered out, 0 ms)

五、总结

通过这次改造,我们获得了一个类似 Spring AOP 的测试环境:

  1. 继承 (Inheritance): 实现了全局/组级别的生命周期控制 (init, destroy)。

  2. RAII: 实现了方法级别的环绕通知 (Around),非常适合做性能计时临时状态保护

这也再次印证了架构设计的核心思想:语言只是工具。只要理解了底层原理(如 CppUTest 的宏展开机制),我们就能用 C++ 的特性,降维打击解决嵌入式 C 测试中的架构痛点。