在测试过程中,经常需要进行mock
当我们测试一个函数时,它还依赖其他的函数实现,比如我们测试api层或者mw层,它会调用底层hal的代码,但这时候可能硬件环境还没完全准备好,又不想因此拖慢进度,我们可以对这些依赖的函数进行mock,模拟返回正常值,等到底层ready之后,还是可以直接调用。
又或者你只想测目标函数的逻辑,想去除依赖函数的影响,也需要用到mock。
又或者你想测试更多场景,比如依赖的函数返回错误,看被测函数是否能正确处理这种异常情况。
那么在c语言中怎样进行mock呢,在《修改代码的艺术》这本书中提出了三个方法。我们可以先看下,源代码如下,这里account_update函数里调用了dp_update函数,接下来我们要对dp_update进行mock。
#include <DFHLItem.h>
#include <DHLSRecord.h>
extern int db_update(int, struct DFHLItem *);
void account_update(
int account_no, struct DHLSRecord *record, int activated)
{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
} else {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}
方法一:编译之前
利用C语言的预处理(在编译之前进行Mock)
先引入一个头文件:
#include <DFHLItem.h>
#include <DHLSRecord.h>
extern int db_update(int, struct DFHLItem *);
#include "localdefs.h"
void account_update(int account_no, struct DHLSRecord *record, int activated)
{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
} else {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}
在该头文件中提供一个db_update的定义,注意,使用了#define把db_update展开为一段代码
#ifdef TESTING
...
struct DFHLItem *last_item = NULL;
int last_account_no = -1;
#define db_update(account_no,item) {last_item = (item); last_account_no = (account_no);}
...
#endif
这样C语言编译器可以把所有的db_update都替换成{last_item = (item); last_account_no = (account_no);}, 这段代码会记录下最后的item和account_no,可以供测试中的验证使用
使用宏就会丢失类型安全,如果逻辑复杂的话,很容易出错谨慎使用该方法。
方法二:编译时
使用函数指针(编译期进行mock)
(1)首先写一个函数指针: int (*db_update)(int, struct DFHLItem *)
(2)把原来的db_update 改名为 int db_update_production(int, struct DFHLITem *)
(3) 编写一个mock实现 int db_update_mock(int, struct DFHLITem *)
(4) 最后使用条件编译来决定到底用哪个函数
#ifdef TESTING
db_update = db_update_mock
#else
db_update = db_update_production
#endif
该方法很灵活, 可以随意通过函数指针进行替换,还能兼顾类型安全 ,推荐使用。
方法三: 编译后
Link时候进行替换
这就需要编写包括db_update的库函数,在link的时候使用这个假的库函数。 当然Link出来的exe文件指示一个测试版本。
如果需要函数很多, 还有db_insert, db_delete等等, 这些函数都需要在假的库函数中进行实现, 开销不小。
方法四: 使用CMOCKA框架
方法:
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "your_header_file.h" // 包含 cv_mw_sensor_init 和相关声明的头文件
// Mock generic_sns_hal_init to return NULL
void *generic_sns_hal_init(jx_u8_t id, void *arg)
{
(void)id;
(void)arg;
return NULL;
}
static void test_cv_mw_sensor_init_failure(void **state)
{
(void)state;
jx_err_t result = cv_mw_sensor_init();
assert_int_equal(result, JX_ERROR);
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_cv_mw_sensor_init_failure),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
编译和运行测试
为了编译和运行这个测试程序,你需要链接 CMocka 库。确保你已经安装了 CMocka,然后使用以下命令进行编译和运行:
bash复制代码gcc -o test_cv_mw_sensor_init test_cv_mw_sensor_init.c -lcmocka
./test_cv_mw_sensor_init
实践
综上,针对我们的代码可以采用方法二:函数指针
需要测试的函数:cv_mw_sensor_init
测试用例:
-
1. 正常初始化
-
2. sub_hal返回NULL(即generic_sns_hal_init函数返回NULL)
原代码:
jx_err_t cv_mw_sensor_init(void)
{
jx_u8_t sub_id = 0;
struct sub_hal *sub_hal = NULL;
struct callbacks m2h_cbs = {
.post_events = post_events,
.register_type = cv_mw_sensor_type_register,
.unregister_type = cv_mw_sensor_type_unregister,
.add_dev = cv_mw_sensor_add_dev,
.del_dev = cv_mw_sensor_del_dev,
};
if (s_init_done) {
GXRLOG_PR_INFO(TAG, "%s init done!\n", func);
return JX_OK;
}
sub_hal = generic_sns_hal_init(sub_id, &m2h_cbs);
if (sub_hal) {
jx_list_insert_after(&s_subhal_list, &sub_hal->node);
} else {
GXRLOG_PR_ERR(TAG, "%s is NULL!\n", func);
return JX_ERROR;
}
init_sensor_list();
s_init_done = JX_TRUE;
return JX_OK;
}
首先可以定义一个函数指针generic_sns_hal_init_ptr,指向原始的函数,再定义一个mock_generic_sns_hal_init,写上return NULL的逻辑
void *(*generic_sns_hal_init_ptr)(jx_u8_t id,void *arg) = generic_sns_hal_init;
void *mock_generic_sns_hal_init(jx_u8_t id, void *arg)
{
(void)id;
(void)arg;
return NULL;
}
接着将原来cv_mw_sensor_init中的函数替换成函数指针
if (s_init_done) {
GXRLOG_PR_INFO(TAG, "%s:already init done!\n", __func__);
return JX_OK;
}
sub_hal = generic_sns_hal_init_ptr(sub_id, &m2h_cbs);
if (sub_hal) {
jx_list_insert_after(&s_subhal_list, &sub_hal->node);
} else {
GXRLOG_PR_ERR(TAG, "%s:subhal is NULL!\n", __func__);
return JX_ERROR;
}
之后在测试代码中
测试用例1,可以照常写
测试用例2,可以写成
jx_void_t init_test_failed_1()
{
generic_sns_hal_init_ptr = mock_generic_sns_hal_init;
jx_err_t err;
err = cv_mw_sensor_init();
if(err)
{
GXRLOG_PR_CRIT(TAG, "failed to init sensorhub!, err = %d\n", err);
}
uexpect_int_equal(err, JX_ERROR);
}
在原教程中,使用了条件编译,适用于,需要隔离底层代码时,将所有未实现的底层代码都返回true。
在我的这个情况下,我的许多case都要同时跑,不同case想要用的函数不同,我就希望在跑case过程中再决定用哪个函数。
此外,在这个过程中,修改了原来的文件,目前来看,没法不修改原代码
另外,也可以设计一个mock直接默认返回正确
// 模拟的成功版本的 generic_sns_hal_init
void *mock_generic_sns_hal_init_success(jx_u8_t id, void *arg) {
struct sub_hal *sub_hal = (struct sub_hal *)malloc(sizeof(struct sub_hal));
if (sub_hal) {
sub_hal->hal.id = id;
}
return sub_hal;
}