C语言实现MOCK

1,943 阅读5分钟

在测试过程中,经常需要进行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;
}