2010年十月月 发布的文章

lcut增加对mock的支持

记得恰好是在一个月前的今天,我发布了lcut(轻量级C语言单元测试框架)0.1.0版本
。由于发布仓促,文档没能及时跟上。在stackoverflow的一个关于单元测试的帖子
上,一位叫Craig McQueen的朋友也给出了建议:"Some documentation would be helpful. Project background and goals, a features list, advantages over existing alternatives, etc would be helpful for people who are checking it out for the first time." 看完这个建议后心里那个汗啊!不过一想到用E文编写文档心里就有些打怵。就这样在这一个月里文档依旧没有改观:(。不过,lcut本身还是有一些进步的,这两天一直规划着为lcut增加mock的支持,今天终于将这个功能加进了lcut,并发布了lcut-0.2.0-beta版,欢迎大家试用,并提出意见和建议。

之前在单元测试过程中使用cmockery中提供的mock功能,cmockery也是lcut的mock功能的直接灵感来源。与cmockery不同的是lcut将对输出参数的mock和对函数返回值的mock区分开来,这样用起来更加直观。

这里用一个简单的例子(完整代码在lcut包product_database_test.c文件中)来说明一下lcut的mock功能如何使用:

/* product_database.c */
int get_total_count_of_employee() {
    database_conn *conn = NULL;
    int retv = -1;
    int total_count = -1;

    conn = connect_to_database("tonybai", "tonybai", "mysql");
    if (!conn)
        return -1;

    retv = table_row_count(conn, "EMPLOYEE_TABLE", &total_count);
    if (retv < 0)
        return -1;
    return total_count;
}

/* product_database_test.c */
database_conn *connect_to_database(const char *user,
                                   const char *passwd,
                                   const char *serviceid) {
    return (database_conn*)LCUT_MOCK_RETV();
}

int table_row_count(const database_conn *conn,
                    const char *table_name,
                    int *total_count) {
    (*total_count) = (int)LCUT_MOCK_ARG();
    return (int)LCUT_MOCK_RETV();
}

void tc_get_total_count_of_employee_ok(lcut_tc_t *tc, void *data) {
    LCUT_RETV_RETURN(connect_to_database, 0×1234);
    LCUT_ARG_RETURN(table_row_count, 5);
    LCUT_RETV_RETURN(table_row_count, 0);

    LCUT_INT_EQUAL(tc, 5, get_total_count_of_employee());
}

被mock的函数多为系统API或执行代价较高的第三方库函数,我们在业务代码更关心的是这些函数的接口行为,而C语言中函数的接口行为表现为:返回值和输出参数。我们需要通过控制被mock函数的接口行为来达到测试我们业务代码的目的,所以我们需要mock这些函数的返回值和输出参数。上面例子中connect_to_database和table_row_count就是两个被mock了的库函数。我们通过LCUT_MOCK_RETV来mock函数的返回值,通过LCUT_MOCK_ARG来mock函数的输出参数。在测试代码tc_get_total_count_of_employee_ok中,我们分别通过LCUT_RETV_RETURN和LCUT_ARG_RETURN来控制前面两个被mock的函数中mock obj的返回值和输出参数: LCUT_RETV_RETURN(connect_to_database, 0×1234)告诉connect_to_database返回(int)0×1234,相应的,LCUT_ARG_RETURN(table_row_count, 5)则告诉table_row_count执行后其输出参数*total_count的值为5。有了这些设定的mock obj我们就可以专注于我们业务层代码的白盒逻辑单元测试了,一旦connect_to_database和table_row_count的外部行为被控制后,业务层的代码get_total_count_of_employee的行为也就是可预期的了,我们用断言测试即可。

由于实现原理限制,如果你的函数输出参数类型或返回值类型为double*/float*,那么这个函数还不能使用lcut的mock功能,否则会编译出错。但绝大多数软件开发领域都很少使用浮点计算,所以lcut的mock还是可以满足大多数需要的。

题外话:
在公司使用代理上网,svn无法直接访问google code,这个问题一直困扰着我,直到今天才知道可以为svn客户端设置代理,设置步骤如下:
-> vi ~/.subversion/servers
-> 增加如下设置:
   [global]
   http-proxy-host = 你的代理主机域名或ip
   http-proxy-port = 端口
   http-proxy-username = 你的用户名
   http-proxy-password = 你的密码
设置后,svn立马就可以连上google code的svn server了。

关于Makefile.am中与Build相关的变量设置

今天尝试使用autoconf和automake重新构建一个遗留库的Build环境。之前改造的lcut的目录结构还是相对简单,改造时并未遇到什么难题,不过今天就没那么幸运了,我在头文件目录包含设置这个看似简单的环节上遇到了一些小麻烦。

这个库结构其实也没那么复杂,只是源文件和头文件不在一个目录下罢了:
testproj/
    – Makefile.am
    – configure.in
    – include/
        – xx.h
        – yy.h
    – module1
        – xx.c
        – Makefile.am
    – moudle2
        – yy.c
        – Makefile.am
   
开始也没多想,参照以前的经验一步一步生成configure脚本。执行configure脚本生成Makefile文件,敲入make。在进入module1目录后,提示编译xx.c文件失败,无法找到xx.h!看了一下gcc的编译选项,的确没有-I上层的include目录,只有"-I."和"-I.."。翻看了一下automake的manual,发现automake默认情况下是将config.h所在的目录当作-I的参数。我的configure.in中是这样设置的:AC_CONFIG_HEADERS([config.h]),怪不得无法正确设置目录呢!将该句改为AC_CONFIG_HEADERS([include/config.h])后,重新生成Makefile并执行make,这回gcc命令行上出现了"-I../include"的字样,编译也很是顺利。

不过就这样算了,似乎总觉不妥,config.h只有一个,但如果有多个include目录的情况下该如何设置头文件包含目录呢?带着这个问题我再次翻看了automake的手册。老天不负有心人^_^,手册里确有这方面的说明。

原来automake从autoconf里继承了很多编译时需要的变量,诸如CC, CFLAGS, CPPFLAGS, DEFS, LDFLAGS,LIBS等等。但automake也可自己设置一些编译时用到的变量,automake与Build相关的一些变量名字也都以AM_开头,诸如AM_CPPFLAGS(与CPPFLAGS对应)。在Makefile.am中设置头文件包含的方式至少有以下两种:

* 在顶层Makefile.am中设置全局变量
AM_CPPFLAGS = -I $(top_srcdir)/include1
export AM_CPPFLAGS
这样在编译子目录(如module1)时,该全局设置也会起作用,在gcc编译命令行中你会看到-I ../include1。

* 在子目录层Makefile.am中设置局部变量
AM_CPPFLAGS = -I $(top_srcdir)/include2
这里的设置仅仅影响该目录下源文件的编译,对于其他同级目录下的源文件不起作用。另外如果此时顶层的Makefile.am中依然有AM_CPPFLAGS的设置,那么子目录下的Makefile.am中的这些设置会覆盖掉顶层的定义,在gcc编译命令行中也只会看到-I include2而无-I include1。

除了在Makefile.am中手工显式设置外,也可在执行configure脚本的时候通过传入CPPFLAGS参数来设定包含头文件位置,如configure CPPFLAGS=-I./include3。注意"CPPFLAGS"、"="和后面的值之间不能有空格。在automake manual中也有这方面的说明:在命令行中这里的CPPFLAGS将被放到AM_CPPFLAGS后面并一起传给gcc。

对于automake中的其他Build相关的AM_XXFLAGS变量,其道理也是相同的,这里就不赘述了。

由bool类型引发的一个问题

C99 原生支持布尔类型,类型名字为_Bool。对C程序员来说,这个名字有些“不伦不类”,还好一般C标准库 实现的头文件中都用宏bool来替代_Bool。C99虽说是C语言当前的最新标准,但是它也有10年历史之久了。据说C1x标准 正在讨论制定中,有兴趣的朋友可以到标准C工作组 官方站点上去瞧瞧。

有些跑题了^_^!其实这篇文章想说的不是C1x标准,而是一个与布尔类型有关的问题的分析解决过程。

上周为项目的复用库增加了一个小功能,对外表现形式就是一组函数。使用lcut 对这组函数进行了详尽的单元测试 ,所有用例都顺利通过。今天和一位同事交流后,觉得应该对这个功能作些改动,针对一些异常情况作些完善。修改方案很简单,就是在一个外部可见的结构体里增加一个表示当前状态的布尔类型的字段,然后在各个函数接口中设置该字段的值并根据该字段的值做相应的处理。

按照既定的思路修改后,原先的用例依旧可以全部pass。继续修改单元测试代码,增加针对此次改动的用例。编译并运行测试,这次则没有那么幸运-有几个用例失败了。查看失败原因,确有一两个是因为逻辑上的问题导致。

修正后,继续运行测试,依旧有两个用例无法通过。 仔细查看了一遍库代码以及单元测试代码,没有发现明显的错误。将LCUT_TRUE断言换成LCUT_INT_EQUAL断言,重新运行测试,发现期望值为true的断言,实际值却是一个-3146789这样的大数。看到这种情况我的第一反应是:是不是内存被污染了?比如代码里有内存覆盖或Buffer溢出的情况。又仔细浏览了一遍代码,依旧没有发现蛛丝马迹。采用gdb 单步执行测试程序,无奈lcut采用了许多回调函数,导致在gdb中无法追踪到我期望的符号。未果后,我尝试换成最原始的增加打印日志输出的调试方式,终于发现了问题端倪。

具体是这样的:在库代码和测试用例代码中,我都输出了bool类型的size,但结果却大相径庭。库代码中输出的sizeof(bool)等于1,而在测试程序中输出的值却为4,这个长度差异直接导致了前面的-3146789的出现。

这里我要补充一下,C99的布尔类型(bool)在stdbool.h中定义:#define bool _Bool(注: _Bool是原生的),这只有在C99下才生效。考虑到有些编译器不支持C99或默认语言标准不是C99,为了兼容,自定义了一份bool的定义,并通过预编译宏与标准定义隔开:

#if __STDC_VERSION__ >= 199901L
#include
#else
#undef  bool
#undef  true
#undef  false
typedef enum {
        false,
        true
} bool;
#endif /* __STDC_VERSION__ >= 199901L */

难道原因是库中代码采用了标准bool类型,而测试代码中采用的是自定义的bool类型?似乎没道理啊。突然间看到了屏幕上编译测试代码的gcc命令行输出:
gcc std=c99 -c -o testall.o testall.c … (后面还有较长的链接库的参数,这里省略)
gcc -o testall testall.c …

怎么对testall.c编译了两次呢?测试代码目录下的Makefile并没有包含第一个gcc命令啊,又翻了翻顶层目录下的Makefile,我找到了答案:原来顶层目录下的Makefile中采用了如下脚本:
OBJ = $(SRC:.c=.o)
${OBJ}: ${SRC}
    ${CC} ${CFLAGS} -c ${SRC}
脚本根据.c文件替换获得.o文件名,并在同名.o和.c间建立依赖,这样所有.c文件都会被先编译为.o文件。

不过第一次编译的结果显然做了无用功,因为第二行命令执行后会覆盖第一行命令生成的.o文件。但恰恰第二行gcc命令中没有加入-std=c99的编译选项,导致testall.c这个编译单元中的bool使用了自定义的bool,导致了其长度为4个字节。

真相终于大白,就是因为testall.c所在目录下的Makefile编写时忘记添加-std=c99选项才导致了上述的问题。又检查了一下其他的存放单元测试代码的目录,发现所有Makefile都存在此问题。以前在没有使用bool类型时这样的Makefile是不会有问题的,关键就是这次我们用了bool类型,问题才暴露出来。

使用C语言,你就不得不常常与指针内存问题、编译器或链接器 问题做斗争,其中的痛苦你最清楚,但处理这些事的过程中所蕴含的快乐也只有你自己最能体会到。继续痛并快乐着吧!




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多