6.3 测试框架 Atest
6.3 测试框架 Atest
📚 本节导读
学习时长: 约 45 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言宏定义、函数指针、结构体、第4章内核组件
🎯 学习目标
- 理解 Atest(And-Test)单元测试框架的设计理念与三级层次结构
- 掌握
atest_tc_entry_t测试用例入口结构体及其注册导出机制 - 掌握 12 种断言宏的使用场景与语义
- 掌握测试单元(TestUnit)与测试点(TestPoint)的执行流程
- 了解 Atest 的 Kconfig 配置项与时钟系统集成
- 能够编写完整的测试用例并进行测试结果统计
一、概述
Atest(And-Test)是 OneOS 内置的轻量级单元测试框架,专为嵌入式实时操作系统设计。它提供了一套完整的测试用例组织、注册、执行与统计机制,帮助开发者在目标平台上直接运行测试用例,验证内核组件和应用代码的正确性。
Atest 框架的核心设计特点:
- 三级层次结构:TestCase(TC,测试用例) -> TestUnit(TU,测试单元) -> TestPoint(TP,测试点),层次清晰,粒度可控
- 段导出机制:利用链接器段(section)
AtestTcTab,测试用例在编译时自动注册,无需手动调用注册函数 - 丰富的断言宏:支持整数、字符串、缓冲区、范围等多种类型的断言,覆盖常见测试场景
- 失败即停止:测试单元内任一测试点失败,立即终止当前测试用例,避免级联错误
- 优先级分类:支持 HIGH/MIDDLE/LOW 三级优先级,可按优先级选择性执行测试用例
- 双日志模式:支持 Dlog 日志系统输出(带颜色)或裸
os_kprintf控制台输出(红/黄/绿/默认色)
二、核心数据结构
2.1 测试用例优先级枚举 enum atest_tc_priority
测试用例支持三种优先级,测试执行时可按优先级排序或筛选(源码 atest.h 第68-73行):
enum atest_tc_priority
{
TC_PRIORITY_HIGH = 0, /* 高优先级测试用例 */
TC_PRIORITY_MIDDLE, /* 中优先级测试用例 */
TC_PRIORITY_LOW, /* 低优先级测试用例 */
TC_PRIORITY_CNT_MAX
};2.2 测试结果统计结构体 atest_stats_t
用于统计多个测试用例执行后的总体结果(源码 atest.h 第83-91行):
struct atest_stats
{
uint16_t tc_passed_num; /* 通过的测试用例总数 */
uint16_t tc_failed_num; /* 失败的测试用例总数 */
uint16_t tu_passed_num; /* 通过的测试单元总数 */
uint16_t tu_failed_num; /* 失败的测试单元总数 */
uint16_t tp_passed_num; /* 通过的测试点总数 */
uint16_t tp_failed_num; /* 失败的测试点总数 */
};
typedef struct atest_stats atest_stats_t;2.3 测试点统计结构体 atest_tp_stats_t
用于统计单个测试用例内的测试点执行结果(源码 atest.h 第101-105行):
struct atest_tp_stats
{
uint16_t tp_passed_num; /* 单用例中通过的测试点总数 */
uint16_t tp_failed_num; /* 单用例中失败的测试点总数 */
};
typedef struct atest_tp_stats atest_tp_stats_t;2.4 测试用例入口结构体 atest_tc_entry_t
每个测试用例编译时被导出到 AtestTcTab 段中,框架通过遍历该段收集所有测试用例(源码 atest.h 第115-123行):
struct atest_tc_entry
{
const char *name; /* 测试用例名称 */
os_err_t (*init)(void); /* 测试用例执行前的初始化函数 */
void (*tc)(void); /* 测试用例执行函数 */
os_err_t (*cleanup)(void); /* 测试用例执行后的清理函数 */
enum atest_tc_priority priority; /* 测试用例优先级 */
};
typedef struct atest_tc_entry atest_tc_entry_t;字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
name | const char * | 测试用例名称,用于日志输出和结果展示 |
init | os_err_t (*)(void) | 初始化回调,返回 OS_SUCCESS 表示成功,否则测试用例不执行 |
tc | void (*)(void) | 测试用例主体函数,内部使用 ATEST_UNIT_RUN 运行测试单元 |
cleanup | os_err_t (*)(void) | 清理回调,测试用例执行完毕后调用(无论成功与否) |
priority | enum atest_tc_priority | 测试用例优先级 |
三、测试用例注册与执行
3.1 ATEST_TC_EXPORT —— 测试用例导出宏
将测试用例入口结构体导出到链接器段 AtestTcTab 中(源码 atest.h 第141-143行):
#define ATEST_TC_EXPORT(name, testcase, init, cleanup, priority) \
OS_USED static const atest_tc_entry_t gs_atest##testcase OS_SECTION( \
"AtestTcTab") = {#name, init, testcase, cleanup, priority};用法示例:
/* 声明测试用例函数 */
static os_err_t math_tc_init(void);
static void math_tc(void);
static os_err_t math_tc_cleanup(void);
/* 导出测试用例 */
ATEST_TC_EXPORT(math_test, math_tc, math_tc_init, math_tc_cleanup, TC_PRIORITY_HIGH);关键点:
- 变量名
gs_atest##testcase通过##拼接,确保唯一性 OS_SECTION("AtestTcTab")指示链接器将该变量放入指定段OS_USED防止编译器优化掉未显式引用的变量#name将参数名转为字符串,作为测试用例名称
3.2 ATEST_UNIT_RUN —— 测试单元执行宏
测试用例主体函数中使用此宏运行测试单元,失败后自动 return(源码 atest.h 第154-162行):
#define ATEST_UNIT_RUN(test_unit_func) \
do \
{ \
atest_unit_run(test_unit_func, #test_unit_func); \
if (atest_tp_stats_get()->tp_failed_num != 0) \
{ \
return; \
} \
} while (0)执行逻辑:
- 调用
atest_unit_run()执行测试单元函数,并统计测试点通过/失败数 - 检查
tp_failed_num,若不为0则立即从测试用例返回,不再执行后续测试单元 - 这种"失败即停止"策略避免了级联错误,便于定位问题根源
3.3 三级层次结构
TestCase (TC) 测试用例,最高层,一个 TC 可包含多个 TU
└── TestUnit (TU) 测试单元,中层,一个 TU 可包含多个 TP
└── TestPoint (TP) 测试点,最底层,一个断言即为一个 TP- TestCase:通过
ATEST_TC_EXPORT注册,包含init/tc/cleanup三个生命周期函数 - TestUnit:通过
ATEST_UNIT_RUN在tc函数中调用,通常对应一个独立的功能验证场景 - TestPoint:通过断言宏(如
tp_assert_true)在 TestUnit 函数中定义,每个断言是一个测试点
四、断言宏详解
Atest 提供 12 种断言宏,覆盖整数、字符串、缓冲区、范围等常见测试场景(源码 atest.h 第167-221行)。所有断言宏通过统一的底层断言函数 atest_assert 或 atest_assert_string / atest_assert_buf 实现。
4.1 真值断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_true(value) | value 为 true(非零) | 基本布尔断言 |
tp_assert_false(value) | value 为 false(零) | 内部对 value 取反后断言 |
tp_assert_true(ret == OS_SUCCESS); /* 检查返回值是否为成功 */
tp_assert_false(task == OS_NULL); /* 检查任务指针是否为空 */4.2 NULL 断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_null(value) | value 为 NULL | 将 value 强转为 const char * 后与 OS_NULL 比较 |
tp_assert_not_null(value) | value 不为 NULL | 同上,取反比较 |
tp_assert_null(os_sem_create("test", 0, OS_IPC_FLAG_FIFO)); /* 创建失败时应返回NULL */
tp_assert_not_null(malloc(128)); /* 内存分配成功时应返回非NULL */4.3 整数断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_integer_equal(a, b) | a == b | 整数相等断言 |
tp_assert_integer_not_equal(a, b) | a != b | 整数不相等断言 |
tp_assert_integer_equal(os_sem_get_value(sem), 5); /* 信号量值应为5 */
tp_assert_integer_not_equal(task->stat, OS_TASK_STAT_RUNNING); /* 任务不应处于运行态 */4.4 字符串断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_str_equal(a, b) | 字符串 a 与 b 内容相等 | 调用 atest_assert_string() 实现 |
tp_assert_str_not_equal(a, b) | 字符串 a 与 b 内容不相等 | equal 参数传 OS_FALSE |
tp_assert_str_equal(task->name, "task_test"); /* 任务名称应为 "task_test" */
tp_assert_str_not_equal(version, "v1.0.0"); /* 版本号不应为 "v1.0.0" */4.5 缓冲区断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_buf_equal(a, b, size) | 缓冲区 a 与 b 前 size 字节相等 | 调用 atest_assert_buf() 实现 |
tp_assert_buf_not_equal(a, b, size) | 缓冲区 a 与 b 前 size 字节不相等 | equal 参数传 OS_FALSE |
uint8_t expected[4] = {0xAA, 0xBB, 0xCC, 0xDD};
tp_assert_buf_equal(rx_buffer, expected, 4); /* 接收缓冲区应与期望值一致 */
tp_assert_buf_not_equal(tx_buffer, rx_buffer, 256); /* 发送和接收缓冲区应不同 */4.6 范围断言
| 宏 | 通过条件 | 备注 |
|---|---|---|
tp_assert_in_range(value, min, max) | value 在 [min, max] 范围内 | 闭区间判断 |
tp_assert_not_in_range(value, min, max) | value 不在 [min, max] 范围内 | 取反判断 |
tp_assert_in_range(adc_value, 0, 4095); /* ADC值应在12位范围内 */
tp_assert_not_in_range(temperature, -40, 125); /* 温度不应在正常范围,说明异常 */五、核心API详解
5.1 atest_unit_run
void atest_unit_run(atest_unit_func_t func, const char *unit_func_name);执行测试单元函数 func,并统计该单元内的测试点通过/失败情况。unit_func_name 为函数名,用于日志输出。
5.2 atest_tp_stats_get
atest_tp_stats_t *atest_tp_stats_get(void);获取当前测试用例的测试点统计指针。ATEST_UNIT_RUN 宏内部调用此函数来判断是否继续执行后续测试单元。
5.3 atest_assert —— 底层断言函数
void atest_assert(os_bool_t condition, const char *file, int32_t line, const char *func, const char *msg);所有真值/整数/范围断言宏的底层实现。当 condition 为 OS_FALSE 时,记录失败并输出文件名、行号、函数名和错误信息。
5.4 atest_assert_string —— 字符串断言
void atest_assert_string(const char *str_a, const char *str_b, os_bool_t equal,
const char *file, int32_t line, const char *func, const char *msg);当 equal == OS_TRUE 时,str_a 与 str_b 不相等则断言失败;当 equal == OS_FALSE 时,str_a 与 str_b 相等则断言失败。
5.5 atest_assert_buf —— 缓冲区断言
void atest_assert_buf(const uint8_t *buff_a, const uint8_t *buff_b, os_size_t size,
os_bool_t equal, const char *file, int32_t line, const char *func, const char *msg);逐字节比较 buff_a 和 buff_b 的前 size 字节。equal 参数含义同 atest_assert_string。
六、日志集成
Atest 框架支持两种日志输出模式,通过 ATEST_USING_DLOG 配置项切换(源码 atest.h 第35-59行)。
6.1 启用 Dlog 日志
当 ATEST_USING_DLOG 定义为 y 时,使用 Dlog 日志系统的宏:
#define ATEST_LOG_E LOG_E /* 错误日志 */
#define ATEST_LOG_W LOG_W /* 警告日志 */
#define ATEST_LOG_I LOG_I /* 信息日志 */
#define ATEST_LOG_D LOG_D /* 调试日志 */
#define ATEST_LOG_RAW LOG_RAW /* 原始日志 */
#define ATEST_LOG_HEX LOG_HEX /* 十六进制日志 */6.2 未启用 Dlog(默认模式)
当 ATEST_USING_DLOG 未定义时,使用 os_kprintf 带颜色输出:
| 宏 | 颜色 | 说明 |
|---|---|---|
ATEST_LOG_E | 红色 \033[31m | 错误日志(断言失败) |
ATEST_LOG_W | 黄色 \033[33m | 警告日志 |
ATEST_LOG_I | 绿色 \033[32m | 信息日志(测试通过) |
ATEST_LOG_D | 默认色 | 调试日志 |
所有日志格式均为:[tick值] 消息 [函数名][行号],其中 tick 值由 os_tick_get_value() 获取,便于在时间轴上定位测试事件。
七、Kconfig 配置
Atest 的配置项定义在 components/atest/Kconfig 中,通过 menuconfig 图形化配置:
menu "Atest"
config OS_USING_ATEST
bool "Enable atest"
default y
select OS_USING_SHELL ← 自动启用 Shell
if OS_USING_ATEST
config ATEST_USING_DLOG
bool "Using dlog"
default n
select OS_USING_DLOG
config ATEST_TASK_STACK_SIZE
int "The atest task stack size"
default 4096
config ATEST_TASK_PRIORITY
int "The atest task priority"
default 5 if OS_TASK_PRIORITY_8
default 10 if OS_TASK_PRIORITY_16
default 20 if OS_TASK_PRIORITY_32
default 40 if OS_TASK_PRIORITY_64
default 80 if OS_TASK_PRIORITY_128
default 160 if OS_TASK_PRIORITY_256
endif
endmenu配置项说明:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
OS_USING_ATEST | bool | y | 启用 Atest 测试框架,自动选择 OS_USING_SHELL |
ATEST_USING_DLOG | bool | n | 使用 Dlog 日志系统输出,启用后自动选择 OS_USING_DLOG |
ATEST_TASK_STACK_SIZE | int | 4096 | 测试任务栈大小(字节),可根据测试用例复杂度调整 |
ATEST_TASK_PRIORITY | int | 视优先级位数而定 | 测试任务优先级,与系统优先级位数联动 |
八、使用示例
8.1 完整测试用例示例
以下示例展示如何编写一个完整的测试用例,包含 init、tc、cleanup 三个生命周期函数:
#include <atest.h>
#include <os_sem.h>
/* 全局变量 */
static os_semaphore_id g_test_sem = OS_NULL;
/* 初始化函数:创建测试资源 */
static os_err_t sem_tc_init(void)
{
g_test_sem = os_sem_create("test_sem", 1, OS_IPC_FLAG_FIFO);
if (OS_NULL == g_test_sem)
{
return OS_FAILURE;
}
return OS_SUCCESS;
}
/* 测试单元1:信号量获取测试 */
static void sem_take_tu(void)
{
/* 测试点1:信号量初始值应为1 */
tp_assert_integer_equal(os_sem_get_value(g_test_sem), 1);
/* 测试点2:获取信号量应成功 */
tp_assert_true(os_sem_wait(g_test_sem, OS_NO_WAIT) == OS_SUCCESS);
/* 测试点3:获取后信号量值应为0 */
tp_assert_integer_equal(os_sem_get_value(g_test_sem), 0);
}
/* 测试单元2:信号量释放测试 */
static void sem_post_tu(void)
{
/* 测试点1:释放信号量应成功 */
tp_assert_true(os_sem_post(g_test_sem) == OS_SUCCESS);
/* 测试点2:释放后信号量值应为1 */
tp_assert_integer_equal(os_sem_get_value(g_test_sem), 1);
}
/* 测试用例主体函数 */
static void sem_tc(void)
{
ATEST_UNIT_RUN(sem_take_tu); /* 执行测试单元1 */
ATEST_UNIT_RUN(sem_post_tu); /* 执行测试单元2 */
}
/* 清理函数:销毁测试资源 */
static os_err_t sem_tc_cleanup(void)
{
if (os_sem_destroy(g_test_sem) != OS_SUCCESS)
{
return OS_FAILURE;
}
return OS_SUCCESS;
}
/* 导出测试用例 */
ATEST_TC_EXPORT(semaphore_test, sem_tc, sem_tc_init, sem_tc_cleanup, TC_PRIORITY_HIGH);8.2 断言使用示例
/* === 整数断言 === */
int result = os_task_create(&task, "worker", task_entry, OS_NULL, 1024, 5);
tp_assert_integer_equal(result, OS_SUCCESS); /* 任务创建应成功 */
/* === 字符串断言 === */
const char *name = os_task_get_name(&task);
tp_assert_str_equal(name, "worker"); /* 任务名称应匹配 */
/* === 缓冲区断言 === */
uint8_t tx_data[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t rx_data[4];
/* ... 通过某种方式接收数据到 rx_data ... */
tp_assert_buf_equal(rx_data, tx_data, 4); /* 收发数据应一致 */
/* === 范围断言 === */
uint32_t tick = os_tick_get_value();
tp_assert_in_range(tick, 0, 10000); /* tick值应在合理范围内 */
/* === NULL 断言 === */
void *ptr = os_malloc(256);
tp_assert_not_null(ptr); /* 内存分配应成功 */8.3 运行结果示例
测试用例执行时,Atest 框架会在控制台输出类似以下格式的日志:
[12345]========================= Run TestCase: semaphore_test =========================
[12346][passed] sem_take_tu - tp1: (os_sem_get_value(g_test_sem)) equal to (1)
[12347][passed] sem_take_tu - tp2: (os_sem_wait(g_test_sem, OS_NO_WAIT) == OS_SUCCESS) is true
[12348][passed] sem_take_tu - tp3: (os_sem_get_value(g_test_sem)) equal to (0)
[12349][passed] sem_post_tu - tp1: (os_sem_post(g_test_sem) == OS_SUCCESS) is true
[12350][passed] sem_post_tu - tp2: (os_sem_get_value(g_test_sem)) equal to (1)
[12351]========================= TestCase Result: PASSED =========================📝 本节小结
本节详细介绍了 OneOS 的 Atest 单元测试框架。Atest 采用 TestCase -> TestUnit -> TestPoint 三级层次结构,通过 ATEST_TC_EXPORT 宏将测试用例编译时导出到 AtestTcTab 段,无需手动注册。框架提供 12 种断言宏覆盖真值、NULL、整数、字符串、缓冲区和范围等测试场景,支持"失败即停止"策略。日志系统支持 Dlog 和裸 os_kprintf 双模式,Kconfig 中提供灵活的配置项。
🔗 相关链接
- 源码文件:
components/atest/include/atest.h - Kconfig 配置:
components/atest/Kconfig - 第4章 内核组件详解(信号量、互斥量等 IPC 测试对象)
- 第6.1节 调试工具概述