【单元测试】FFF模拟框架

1. 前言

在学习单元测试的过程中,使用「模拟框架」隔离依赖是一项必须要掌握的技术。目前模拟框架有很多,琳琅满目,参差不齐。针对C语言模拟框架的初学者,我推荐FFF框架,因为该框架简单,易上手,而且有助于初学者掌握模拟框架的幕后原理。有了基础,再去学习更复杂、更高端的框架,就游刃有余了。

注:在众多的C语言模拟框架中,FFF框架算是一个比较冷门的框架,至少截止我书稿本文时是如此,在网上鲜有相关资料,可见算不上主流,但FFF框架很适合模拟框架的初学者,这也正是我撰写本文的目的。

2. FFF框架简介

官网:https://github.com/meekrosoft/fff

FFF全称Fake Function Framework,是一个用于单元测试的C语言轻量型模拟框架。整个框架就一个头文件fff.h,全部用宏定义实现的框架,非常简洁,你只要include该头文件,就能使用该框架了。

FFF框架的优点:

  • 很容易创建C语言的存根和模拟对象。
  • 它很简单,只需包含一个头文件,就可以开始了。

3. 入门体验

3.1 下载fff.h头文件

git clone https://github.com/meekrosoft/fff.git

我们只需要其中的fff.h头文件即可,其他不需要。

说明:克隆下来的代码中有个gtest文件夹,这是谷歌的Google Test单元测试框架,FFF框架是模拟框架,需要区分两者,本文重点讲解的是FFF模拟框架,不会涉及单元测试框架。

3.2 初次体验

直接上例子:

void UI_init(void)
{
    DISPLAY_init();
}

UI_init函数调用了DISPLAY_init接口。在没有DISPLAY_init接口实现代码的情况下(只知道接口的声明,如下所示),如何对UI_init函数进行单元测试?

void DISPLAY_init();

使用FFF框架很容易创建DISPLAY_init模拟函数,只要三行代码:

#include "fff.h"
DEFINE_FFF_GLOBALS;
FAKE_VOID_FUNC(DISPLAY_init);

完整测试代码下如下:

// test.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#include "fff.h"
DEFINE_FFF_GLOBALS;
FAKE_VOID_FUNC(DISPLAY_init);

void UI_init(void)
{
    DISPLAY_init();
}

#define ASSERT_EQ(A, B) assert((A) == (B))

int main()
{
    UI_init();
    ASSERT_EQ(DISPLAY_init_fake.call_count, 1);
    return 0;
}

可以编译成功:

gcc -o test test.c

为何如此神奇,只要两个宏(DEFINE_FFF_GLOBALS 和 FAKE_VOID_FUNC)就能模拟出接口,FFF框架幕后到底干了什么?要解开谜底,只要进行一次宏展开就能知晓。对test.c进行宏展开:

gcc -E -P test.c >> test.prescan

查看test.prescan展开后的代码(只列出两个宏对应的关键代码,略有删减):

/* 以下是 DEFINE_FFF_GLOBALS 宏展开后的代码 */
typedef void(*fff_function_t)(void);
typedef struct {
    fff_function_t call_history[(50u)];
    unsigned int call_history_idx;
} fff_globals_t;
fff_globals_t fff;

/* 以下是 FAKE_VOID_FUNC(DISPLAY_init) 宏展开后的代码 */
typedef struct DISPLAY_init_Fake {
    unsigned int call_count;
    unsigned int arg_history_len;
    unsigned int arg_histories_dropped;
    int custom_fake_seq_len;
    int custom_fake_seq_idx;
    void(*custom_fake)(void);
    void(**custom_fake_seq)(void);
} DISPLAY_init_Fake;
DISPLAY_init_Fake DISPLAY_init_fake;

void DISPLAY_init(void)
{
    if (DISPLAY_init_fake.call_count < (50u)) {
    } else {
        DISPLAY_init_fake.arg_histories_dropped++;
    }
    DISPLAY_init_fake.call_count++;

    if (fff.call_history_idx < (50u))
        fff.call_history[fff.call_history_idx++] = (fff_function_t)DISPLAY_init;;
    if (DISPLAY_init_fake.custom_fake_seq_len) {
        if (DISPLAY_init_fake.custom_fake_seq_idx < DISPLAY_init_fake.custom_fake_seq_len) {
            DISPLAY_init_fake.custom_fake_seq[DISPLAY_init_fake.custom_fake_seq_idx++]();
        } else {
            DISPLAY_init_fake.custom_fake_seq[DISPLAY_init_fake.custom_fake_seq_len - 1]();
        }
    }
    if (DISPLAY_init_fake.custom_fake)
        DISPLAY_init_fake.custom_fake();
}

void DISPLAY_init_reset(void)
{
    memset(&DISPLAY_init_fake, 0, sizeof(DISPLAY_init_fake));
    DISPLAY_init_fake.arg_history_len = (50u);
};

耐心看完以上代码,基本上就能知道是咋回事了。

  • DEFINE_FFF_GLOBALS 宏定义了一个结构体全局变量,用于记录、跟踪函数调用的历史记录。

  • FAKE_VOID_FUNC 宏定义是关键所在,其语法为:

    FAKE_VOID_FUNC(fn [,arg_types*])
    
    • 该宏定义了一个名为fn的模拟函数,模拟函数返回值类型为void,形参列表为arg_types(可选项,不填就表示形参为void)。
    • 该宏还定义了一个名为fn_fake的结构体全局变量,该结构体包含有关模拟函数fn的所有状态信息,例如call_count会在每次调用模拟函数fn时递增。
    • 该宏还定义了一个名为fn_reset的函数,用于重置模拟函数fn的状态。fn_reset函数往往是在执行测试用例前(或后)被调用,即setup或teardown中调用,以免影响其他测试用例,或者被其他测试用例影响。

总结起来就是,示例中 FAKE_VOID_FUNC(DISPLAY_init) 宏定义了一个结构体,两个函数:

  • DISPLAY_init_Fake 结构体
  • void DISPLAY_init(void) 函数
  • void DISPLAY_init_reset(void) 函数

至此,基本上就整明白FFF模拟框架的幕后原理了。

4. 深入学习

接下来,更深入的了解下FFF框架。

4.1 模拟函数形参

如果要定义带有形参的模拟函数,比如:

void DISPLAY_output(char * message);

可以这样:

FAKE_VOID_FUNC(DISPLAY_output, char *);

测试用例(UI_write_line函数会调用DISPLAY_output接口):

void test(void)
{
    char msg[] = "helloworld";
    UI_write_line(msg);
    ASSERT_EQ(DISPLAY_output_fake.call_count, 1);
    ASSERT_EQ(strcmp(DISPLAY_output_fake.arg0_val, msg), 0);
}

在FAKE_VOID_FUNC宏定义中,函数名之后紧接的是函数的形参列表(示例中是char指针),每个形参在fn_fake结构体中都有argN_val变量与之对应(N从0开始)。欲知代码详情,宏展开。

4.2 模拟函数返回值

如果要定义带有函数返回值的模拟函数,应该使用FAKE_VALUE_FUNC宏,其语法为:

FAKE_VALUE_FUNC(return_type, fn [,arg_types*]);
  • return_type是模拟函数fn的返回值类型,为必填项。
  • fn是模拟函数名,为必填项。
  • arg_types是模拟函数fn的形参列表,为可选项。

例如:

unsigned int DISPLAY_get_line_capacity();
unsigned int DISPLAY_get_line_insert_index();

可以这样:

FAKE_VALUE_FUNC(unsigned int, DISPLAY_get_line_capacity);
FAKE_VALUE_FUNC(unsigned int, DISPLAY_get_line_insert_index);

测试用例:

void test(void)
{
    // 设定 DISPLAY_get_line_insert_index 函数预期返回值
    DISPLAY_get_line_insert_index_fake.return_val = 1;

    ASSERT_EQ(DISPLAY_get_line_insert_index(), 1);
}

欲知代码详情,宏展开。模拟更复杂的函数,例如:

double pow(double base, double exponent);

可以这样:

FAKE_VALUE_FUNC(double, pow, double, double);

4.3 重置模拟函数状态

好的单元测试会隔离每个测试用例,因此重置模拟函数fn的状态对每个单元测试都至关重要。每个模拟函数fn都有对应的fn_reset接口,用于重置fn的状态信息和呼叫计数。最好的做法是在测试用例的setup中调用fn_reset以重置模拟函数fn的状态。例如:

void setup(void)
{
    // Register resets
    RESET_FAKE(DISPLAY_init);
    RESET_FAKE(DISPLAY_clear);
    RESET_FAKE(DISPLAY_output_message);
    RESET_FAKE(DISPLAY_get_line_capacity);
    RESET_FAKE(DISPLAY_get_line_insert_index);
    FFF_RESET_HISTORY();
}

RESET_FAKE 宏会调用相应模拟函数的fn_reset接口以重置fn的状态。而 FFF_RESET_HISTORY 宏用于重置函数调用历史记录,后面章节会讲到函数调用历史记录。

4.4 模拟函数调用记录

如果你要测试一个函数,这个函数依次调用了functionA、functionB、functionA接口,想在测试用例中检查接口调用顺序是否符合预期,怎么测?FFF框架内部维护着所有模拟函数的调用历史记录,因此很容易测试。例如:

FAKE_VOID_FUNC(voidfunc2, char, char);
FAKE_VALUE_FUNC(long, longfunc0);

void test(void)
{
    longfunc0();
    voidfunc2();
    longfunc0();

    ASSERT_EQ(fff.call_history[0], (void *)longfunc0);
    ASSERT_EQ(fff.call_history[1], (void *)voidfunc2);
    ASSERT_EQ(fff.call_history[2], (void *)longfunc0);
}

如果要重置函数调用历史记录,可以使用 FFF_RESET_HISTORY() 宏,一般是在setup中调用该宏。

4.5 模拟函数参数记录

默认情况下,框架内部会记录每个模拟函数的最后十次被调用的参数值,每个伪函数的每个参数值都会记录。

void test(void)
{
    voidfunc2('g', 'h');
    voidfunc2('i', 'j');
    ASSERT_EQ('g', voidfunc2_fake.arg0_history[0]);
    ASSERT_EQ('h', voidfunc2_fake.arg1_history[0]);
    ASSERT_EQ('i', voidfunc2_fake.arg0_history[1]);
    ASSERT_EQ('j', voidfunc2_fake.arg1_history[1]);
}

注意,RESET_FAKE 会清除对应伪函数的参数历史记录。

4.6 模拟函数返回值序列

在单元测试中,有时候会多次调用同一个外部依赖函数,并且期望每次调用都返回不同值。FFF框架实现此操作的方法是,为模拟函数指定返回值序列。例如:

// faking "long longfunc();"
FAKE_VALUE_FUNC(long, longfunc0);

void test(void)
{
    long myReturnVals[3] = { 3, 7, 9 };
    SET_RETURN_SEQ(longfunc0, myReturnVals, 3);
    ASSERT_EQ(myReturnVals[0], longfunc0());
    ASSERT_EQ(myReturnVals[1], longfunc0());
    ASSERT_EQ(myReturnVals[2], longfunc0());
    ASSERT_EQ(myReturnVals[2], longfunc0());
    ASSERT_EQ(myReturnVals[2], longfunc0());
}

通过使用SET_RETURN_SEQ宏指定返回值序列,模拟函数将按顺序返回数组中给出的值。当到达序列的末尾时,模拟函数将继续无限期地返回序列中的最后一个值。

4.7 宏备忘录

描述例子
FAKE_VOID_FUNC(fn [,arg_types*]);定义一个模拟函数,函数返回值类型为void,函数名为fn,函数形参为arg_types(可选项)。FAKE_VOID_FUNC(DISPLAY_init);
FAKE_VOID_FUNC(DISPLAY_output_message, const char*);
FAKE_VALUE_FUNC(return_type, fn [,arg_types*]);定义一个模拟函数,函数返回值类型为return_type,函数名为fn,函数形参为arg_types(可选项)。FAKE_VALUE_FUNC(int, DISPLAY_get_line_insert_index);
FAKE_VOID_FUNC_VARARG(fn [,arg_types*], …);定义一个带有可变参数的模拟函数,函数返回值类型为void,函数名为fn,函数形参为arg_types(可选项)。FAKE_VOID_FUNC_VARARG(fn, const char*, …)
FAKE_VALUE_FUNC_VARARG(return_type, fn [,arg_types*], …);定义一个带有可变参数的模拟函数,函数返回值类型为return_type,函数名为fn,函数形参为arg_types(可选项)。FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE*, const char*, …)
RESET_FAKE(fn);重置模拟函数fn的状态信息。RESET_FAKE(DISPLAY_init);

4.8 更多学习

更多学习,可以参阅FFF框架官网中的使用手册(英文):https://github.com/meekrosoft/fff

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值