一、结构与设计思想的不同
1. 2.7.0 (基于 test_main
)
- 设计特点 :
test_main
是一个集中式的入口函数,所有的测试用例通过注册方式放在一个全局范围。- 需要手动维护测试函数列表和初始化代码。
- 局限性 :
- 缺乏模块化和灵活性,复杂测试场景下代码难以管理。
- 没有专门的机制支持前置与后置操作(setup 和 teardown)。
2. 3.7.0 (基于 ZTEST
)
- 设计特点 :
- 引入了模块化的测试套件框架,每个测试套件通过宏进行定义。
- 支持suite_name、before_fn、after_fn、before_each_fn、after_each_fn及suite_data。
- 自动管理测试用例的注册和执行,提供更清晰的结构。
- 优势 :
- 提高了代码的可维护性和可读性。
- 更易于扩展和集成,比如为每个测试用例定义不同的前后处理逻辑。
二、代码示例对比
1. 2.7.0示例 (基于 test_main
)
c
#include <ztest.h>
/* 测试用例函数 */
void test_case1(void) {
zassert_equal(1, 1, "Values are not equal");
}
void test_case2(void) {
zassert_not_null(NULL, "Pointer is NULL");
}
/* 测试套件入口函数 */
void test_main(void) {
/* 注册测试用例 */
ztest_test_suite(my_test_suite,
ztest_unit_test(test_case1),
ztest_unit_test(test_case2)
);
/* 运行测试套件 */
ztest_run_test_suite(my_test_suite);
}
- 特点 :
- 所有测试函数都集中在
test_main
中注册和运行。 - 缺少前后处理逻辑(如测试初始化和资源清理)。
- 所有测试函数都集中在
2. 3.7.0示例 (基于 ZTEST
)
c
#include <zephyr/ztest.h>
/* 前置操作 (Setup) */
static void setup(void *data) {
printk("Setup for test suite\\n");
}
/* 后置操作 (Teardown) */
static void teardown(void *data) {
printk("Teardown for test suite\\n");
}
/* 测试用例函数 */
ZTEST(my_test_suite, test_case1) {
zassert_equal(1, 1, "Values are not equal");
}
ZTEST(my_test_suite, test_case2) {
zassert_not_null(NULL, "Pointer is NULL");
}
/* 定义测试套件 */
ZTEST_SUITE(my_test_suite, NULL, setup, NULL, teardown, NULL);
- 特点 :
- 使用
ZTEST_SUITE
定义测试套件并绑定setup和teardown。 - 测试用例使用
ZTEST(套件名, 测试函数名)
宏定义,自动注册。 - 提供清晰的模块化结构,适合复杂项目。
- 使用
三、高级特性对比
1. 头文件引用差异
c
// 2.7.0 版本
#include <ztest.h>
#include <kernel.h>
#include <drivers/uart.h>
// 3.7.0 版本
#include <zephyr/ztest.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
2. 测试用例组织
c
// 2.7.0 版本 - 通过函数组织
static void test_uart_basic(void) {
// 测试代码
}
// 3.7.0 版本 - 通过宏组织
ZTEST(uart_tests, test_uart_basic) {
// 测试代码
}
3. 生命周期管理
c
// 2.7.0 版本
ztest_unit_test_setup_teardown(test_func, setup_fn, teardown_fn);
// 3.7.0 版本
ZTEST_SUITE(test_suite, NULL,
setup_fn, /* 套件启动前 */
before_fn, /* 每个测试前 */
after_fn, /* 每个测试后 */
teardown_fn /* 套件结束后 */
);
四、区别总结表
特性 | 2.7.0 (test_main ) |
3.7.0 (ZTEST ) |
---|---|---|
测试用例注册方式 | 手动注册 | 自动注册,使用ZTEST 宏定义 |
模块化支持 | 较弱,所有代码集中在test_main |
强,测试套件分离,便于扩展 |
前后处理 (setup/teardown) | 无,需手动在测试函数中实现 | 原生支持,通过ZTEST_SUITE 绑定 |
代码结构可读性 | 较差,复杂项目中难以管理 | 高度清晰,支持复杂项目 |
API 灵活性 | 基础的断言和测试功能 | 丰富的断言和模拟功能 |
适用场景 | 小型简单的测试代码 | 复杂的测试场景,尤其是需要共享状态的场景 |
五、实际应用场景
1. 2.7.0 test_main
适用场景
- 小型项目或简单测试,测试用例数量较少。
- 没有复杂的前置或后置需求。
- 单一功能模块的测试。
2. 3.7.0 ZTEST
适用场景
- 大型项目,测试用例多,结构复杂。
- 需要模块化管理、前置/后置逻辑和共享状态的场景。
- 更易与自动化测试工具(如Twister)集成。
- 需要高度可维护性的测试项目。
六、升级建议
1. 渐进式迁移步骤
- 更新头文件包含方式
- 将测试用例函数改写为ZTEST格式
- 添加生命周期管理函数
- 重构测试套件结构
- 优化测试用例组织
2. 注意事项
- 保持测试逻辑的一致性
- 确保所有测试用例都被正确迁移
- 验证生命周期函数的正确执行
- 检查测试覆盖率的完整性
七、实际应用案例展示
1. UART驱动测试案例对比
2.7.0版本实现
c
/*
* Copyright (c) 2024 Martin.Ma
* SPDX-License-Identifier: Apache-2.0
*/
#include <ztest.h>
#include <kernel.h>
#include <device.h>
#include <drivers/uart.h>
/* 设备定义 */
#if DT_NODE_HAS_STATUS(DT_CHOSEN(zephyr_console), okay)
#define UART_NODE DT_CHOSEN(zephyr_console)
#else
#define UART_NODE DT_NODELABEL(uart0)
#endif
static const struct device *uart_dev;
/* 设备准备函数 */
static void uart_setup(void)
{
uart_dev = DEVICE_DT_GET(UART_NODE);
zassert_not_null(uart_dev, "UART device not found");
zassert_true(device_is_ready(uart_dev), "UART device not ready");
}
/* 测试用例:基本发送功能 */
static void test_uart_basic_tx(void)
{
const char tx_str[] = "Hello UART\\r\\n";
TC_PRINT("Testing basic UART transmission\\n");
TC_PRINT("Sending: %s", tx_str);
/* 发送测试字符串 */
for (int i = 0; i < strlen(tx_str); i++) {
uart_poll_out(uart_dev, tx_str[i]);
k_sleep(K_MSEC(1)); /* 小延时确保稳定性 */
}
/* 这里我们只验证发送调用是否成功完成 */
zassert_true(true, "Basic transmission test completed");
}
/* 测试用例:基本接收功能 */
static void test_uart_basic_rx(void)
{
unsigned char rx_char;
int ret;
TC_PRINT("Testing basic UART reception\\n");
/* 尝试接收一个字符 */
ret = uart_poll_in(uart_dev, &rx_char);
/* 验证返回值正确性 */
zassert_true(ret == 0 || ret == -1,
"uart_poll_in returned unexpected value: %d", ret);
if (ret == 0) {
TC_PRINT("Received character: %c\\n", rx_char);
} else {
TC_PRINT("No data available (expected in this test)\\n");
}
}
/* 测试用例:简单的数据环回测试 */
static void test_uart_loopback(void)
{
const char test_char = 'A';
unsigned char rx_char;
int ret;
TC_PRINT("Testing UART loopback with single character\\n");
/* 发送单个字符 */
TC_PRINT("Sending character: %c\\n", test_char);
uart_poll_out(uart_dev, test_char);
k_sleep(K_MSEC(10)); /* 等待数据传输完成 */
/* 尝试接收字符 */
ret = uart_poll_in(uart_dev, &rx_char);
/* 如果成功接收到数据,验证其正确性 */
if (ret == 0) {
TC_PRINT("Received character: %c\\n", rx_char);
zassert_equal(rx_char, test_char,
"Character mismatch: expected '%c', got '%c'",
test_char, rx_char);
}
}
/* 主测试函数 */
void test_main(void)
{
uart_setup();
ztest_test_suite(uart_pl011_suite,
ztest_unit_test(test_uart_basic_tx),
ztest_unit_test(test_uart_basic_rx),
ztest_unit_test(test_uart_loopback)
);
ztest_run_test_suite(uart_pl011_suite);
}
3.7.0版本实现
c
/*
* Copyright (c) 2024 Martin.Ma
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/ztest.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
/* 设备定义 */
#if DT_NODE_HAS_STATUS(DT_CHOSEN(zephyr_console), okay)
#define UART_NODE DT_CHOSEN(zephyr_console)
#else
#define UART_NODE DT_NODELABEL(uart0)
#endif
static const struct device *uart_dev;
/* 测试用例:基本发送功能 */
ZTEST(uart_pl011, test_uart_basic_tx)
{
const char tx_str[] = "Hello UART\\r\\n";
TC_PRINT("Testing basic UART transmission\\n");
TC_PRINT("Sending: %s", tx_str);
/* 发送测试字符串 */
for (int i = 0; i < strlen(tx_str); i++) {
uart_poll_out(uart_dev, tx_str[i]);
k_sleep(K_MSEC(1)); /* 小延时确保稳定性 */
}
/* 这里我们只验证发送调用是否成功完成 */
zassert_true(true, "Basic transmission test completed");
}
/* 测试用例:基本接收功能 */
ZTEST(uart_pl011, test_uart_basic_rx)
{
unsigned char rx_char;
int ret;
TC_PRINT("Testing basic UART reception\\n");
/* 尝试接收一个字符 */
ret = uart_poll_in(uart_dev, &rx_char);
/* 验证返回值正确性 */
zassert_true(ret == 0 || ret == -1,
"uart_poll_in returned unexpected value: %d", ret);
if (ret == 0) {
TC_PRINT("Received character: %c\\n", rx_char);
} else {
TC_PRINT("No data available (expected in this test)\\n");
}
}
/* 测试用例:简单的数据环回测试 */
ZTEST(uart_pl011, test_uart_loopback)
{
const char test_char = 'A';
unsigned char rx_char;
int ret;
TC_PRINT("Testing UART loopback with single character\\n");
/* 发送单个字符 */
TC_PRINT("Sending character: %c\\n", test_char);
uart_poll_out(uart_dev, test_char);
k_sleep(K_MSEC(10)); /* 等待数据传输完成 */
/* 尝试接收字符 */
ret = uart_poll_in(uart_dev, &rx_char);
/* 如果成功接收到数据,验证其正确性 */
if (ret == 0) {
TC_PRINT("Received character: %c\\n", rx_char);
zassert_equal(rx_char, test_char,
"Character mismatch: expected '%c', got '%c'",
test_char, rx_char);
}
}
/* 设备准备函数 */
static void *uart_setup(void)
{
uart_dev = DEVICE_DT_GET(UART_NODE);
zassert_not_null(uart_dev, "UART device not found");
zassert_true(device_is_ready(uart_dev), "UART device not ready");
return NULL;
}
/* 定义测试套件 */
ZTEST_SUITE(uart_pl011, NULL, uart_setup, NULL, NULL, NULL);
2. FIFO/LIFO测试案例
2.7.0版本实现
c
#define STACK_SIZE 500
#define LIST_LEN 8
static struct k_sem sema;
static struct k_fifo fifo;
static struct k_thread tdata;
static K_THREAD_STACK_DEFINE(tstack, STACK_SIZE);
struct fifo_item {
void *fifo_reserved;
uint8_t value;
};
static struct fifo_item fifo_data[LIST_LEN];
static void fifo_thread_entry(void *p1, void *p2, void *p3)
{
struct fifo_item *rx_data;
/* 从FIFO读取数据 */
for (int i = 0; i < LIST_LEN; i++) {
rx_data = k_fifo_get(&fifo, K_NO_WAIT);
zassert_equal(rx_data->value, fifo_data[i].value, NULL);
}
k_sem_give(&sema);
}
static void test_fifo(void)
{
k_sem_init(&sema, 0, 1);
k_fifo_init(&fifo);
/* 写入测试数据 */
for (int i = 0; i < LIST_LEN; i++) {
fifo_data[i].value = i;
k_fifo_put(&fifo, &fifo_data[i]);
}
/* 创建测试线程 */
k_tid_t tid = k_thread_create(&tdata, tstack, STACK_SIZE,
fifo_thread_entry, &fifo, NULL, NULL,
K_PRIO_PREEMPT(0), 0, K_NO_WAIT);
k_sem_take(&sema, K_FOREVER);
k_thread_abort(tid);
}
void test_main(void)
{
ztest_test_suite(kernel_tests,
ztest_unit_test(test_fifo)
);
ztest_run_test_suite(kernel_tests);
}
3.7.0版本实现
c
#define STACK_SIZE 500
#define LIST_LEN 8
struct fifo_test_fixture {
struct k_sem sema;
struct k_fifo fifo;
struct k_thread tdata;
struct fifo_item {
void *fifo_reserved;
uint8_t value;
} data[LIST_LEN];
};
static K_THREAD_STACK_DEFINE(tstack, STACK_SIZE);
static void fifo_thread_entry(void *arg)
{
struct fifo_test_fixture *fixture = (struct fifo_test_fixture *)arg;
struct fifo_item *rx_data;
/* 从FIFO读取数据 */
for (int i = 0; i < LIST_LEN; i++) {
rx_data = k_fifo_get(&fixture->fifo, K_NO_WAIT);
zassert_equal(rx_data->value, fixture->data[i].value, NULL);
}
k_sem_give(&fixture->sema);
}
static void *fifo_setup(void)
{
struct fifo_test_fixture *fixture = malloc(sizeof(struct fifo_test_fixture));
k_sem_init(&fixture->sema, 0, 1);
k_fifo_init(&fixture->fifo);
return fixture;
}
static void fifo_before(void *data)
{
struct fifo_test_fixture *fixture = (struct fifo_test_fixture *)data;
/* 准备测试数据 */
for (int i = 0; i < LIST_LEN; i++) {
fixture->data[i].value = i;
}
}
static void fifo_teardown(void *data)
{
free(data);
}
ZTEST_F(kernel, test_fifo)
{
/* 写入测试数据 */
for (int i = 0; i < LIST_LEN; i++) {
k_fifo_put(&fixture->fifo, &fixture->data[i]);
}
/* 创建测试线程 */
k_tid_t tid = k_thread_create(&fixture->tdata, tstack, STACK_SIZE,
fifo_thread_entry, fixture, NULL, NULL,
K_PRIO_PREEMPT(0), 0, K_NO_WAIT);
k_sem_take(&fixture->sema, K_FOREVER);
k_thread_abort(tid);
}
ZTEST_SUITE(kernel, NULL, fifo_setup, fifo_before, NULL, fifo_teardown);
这些实际案例展示了两个版本在测试实现上的具体差异:
- 数据管理方式的变化:3.7.0版本使用fixture管理测试数据,而2.7.0使用全局变量
- 生命周期管理的改进:3.7.0提供了更完整的生命周期管理机制
- 代码组织结构的优化:3.7.0的代码结构更清晰、模块化程度更高
- 错误处理的增强:3.7.0提供了更多的错误检查点
八、最佳实践建议
1. 测试代码组织
- 按功能模块组织测试套件
- 合理使用setup和teardown
- 保持测试用例的独立性
- 注意资源的正确清理
2. 异常处理
- 合理使用断言验证结果
- 处理所有可能的错误场景
- 确保资源正确释放
3. 调试技巧
- 使用适当的日志级别
- 合理设置断点
- 验证测试环境的正确性
这个升级过程虽然需要投入一定的工作量,但从长远来看是值得的,因为它能带来更好的代码组织、更强的可维护性和更高的测试效率。建议在进行升级时,采取循序渐进的方式,确保每一步都经过充分的验证。