曾经在多篇blog中报怨过:用C语言写业务逻辑实在是让人身心忐忑不安,再加之C语言自有的"特点",让其与"单元测试"始终若即若离,曾经尝试过写了一个轻量级C Unit Testing lib,至少目前我依旧在用,但多用在编写独立算法以及底层库的场合。业务层少有使用。业务层多是遗留系统,当初前辈们设计时对可测性考虑不够周全,导致现在无法很好的将各个部分独立抽出进行测试,虽然我们也在做着类似"重构"的工作,但鉴于规模较大,不能一蹴而就,我们需要一步一步找出使用C应对各种单元测试情况的方法。这里说说Mock Test。
在系统中,我们不可避免的要调用一些外部或者系统级别的接口,而我们在测试时这些接口的环境也许并不存在,但是我们测试业务流程时还是要覆盖到所有的,业界就提出了Mock这个概念,最开始在Java开发领域,后来在其他语言中都有引入。Mock是一种什么东西?其实感觉就是给你一个机会,一种模拟和控制外部/系统级别对象或者接口的方法,这样你大可在不必与真实环境交互的前提下就完成所有依赖外部环境的业务流程的覆盖测试。Mock Test在许多语言中都有支持,但是在C语言中,Mock的支持似乎少之又少,在Cgreen这个C Unit test framework中虽然支持Mock,但是其要求你的待测试的业务接口必须附加一个stub参数,这样具有"侵入性"的设计让我感觉很是别扭,而且对于外部接口,你更是无法改变其接口原型,那么能否有其他的方法呢?这里放出一种我的方案,也不甚完美。
C语言有其自身的特色,我这里利用的就是其在编译时的一些特色:先进行预编译,再进行编译和链接。我试着在两个阶段之间做一个trick,以达到我的目的-Mock。
一般我们这样编写一个业务模块:
/* biz.h */
#ifndef BIZ_H
#define BIZ_H
#include
int biz_operation(char *fname);
#endif
/* biz.c */
#include "biz.h"
int biz_operation(char *fname) {
FILE *fp = NULL;
fp = fopen(fname, "r");
if (fp == NULL) {
printf("fail to open fle!/n");
return 1;
} else {
printf("succeed to open file!/n");
return 0;
}
}
这个业务模块有一个业务操作流程,试图只读方式打开一个文件。如果没有mock辅助,我们在测试时需要在环境下手工创建一个文件,这样才能返回"成功",否则一直就是"失败","成功"分支也就无法测试得到。
我们先建立我们的单元测试代码文件:
/* test.c */
#include "biz.h"
void test_biz_operation_return_succ() {
int rv;
rv = biz_operation("test-c-mock.txt");
xx_assert(rv == 0);
}
int main() {
... ...
/* 无论采用什么单元测试框架,都会有直接或者间接的类似如下调用:*/
test_biz_operation_return_succ();
... ...
}
biz_operation依赖一个fopen的C标准库调用,我们如何去做一个fopen的mock接口呢?前面说过,我们要利用C的两阶段编译的特色来完成这个mock。我们增加一个mock.h和一个对应的mock.c,我们在这对文件中实现我们自己对fopen的控制。
/* mock.h */
#ifndef MOCK_H
#define MOCK_H
#include
FILE* mock_fopen(char *fname, char *option);
#endif
/* mock.c */
#include "mock.h"
FILE* mock_fopen(char *fname, char *option) {
return (FILE*)0x12345678; //在这里,你可以自由控制返回结果
}
如何将mock_fopen与fopen联系在一起呢?我们通过gcc提供的-D选项来做。我们形象化的说一下:
上述源文件的目录结构如下:
/export/home/mock_test
- biz/
- test/
我们在test目录下新建一个Makefile文件,用来自动完成test的编译。
## Makefile ##
BIZOBJDIR = /export/home/mock/biz
BIZOBJ = $(BIZOBJDIR)/mock_biz.o
BIZSRC = $(BIZOBJLIB)/biz.c
TESTSRC = test.c mock.c
TESTOBJ = test.o mock.o
MOCK_FLAG = -Dfopen=mock_fopen # 关键之处
all:
gcc $(MOCK_FLAG) -c -o $(BIZOBJ) $(BIZSRC)
gcc -c $(TESTSRC) -I$(BIZOBJLIB)
gcc -o test $(TESTOBJ) $(BIZOBJ)
我们从理论上分析一下这种方法的可行性:我们先将biz.c编译成.o文件,MOCK_FLAG将biz.c中的fopen替换成了mock_fopen,这些都是预编译器的功劳;然后我们在test目录下,将test.c 和mock.c编译为对应的.o文件,这里无需使用MOCK_FLAG,否则会有compile error发生;最后一步进行链接:test.o中的biz_operation符号在mock_biz.o中被resolved,而mock_biz.o中的biz_operation在mock.o中被resolved。这样链接后,
fopen处实际上调用的是mock_fopen,也就是那个你可以自由控制的接口。如果在biz.c中还有其他系统调用,比如write, read等,我们都可以
将其mock加到mock.c中,比如称为mock_write和mock_read,然后更新MOCK_FLAG = -Dfopen=mock_fopen -Dwrite=mock_write -
Dread=mock_read等。
以上结果已经在Solaris上GCC下测试通过了,目前这种想法还不成够熟,也只是在单个业务模块下做了些测试,下一步如果能做到整个工程的单元测试那就更好了。鉴于上面的情况,如果mock过多,对Makefile的维护任务将有很大加重,实现全工程的单元测试集中还需时日。