##背景
PHP探针
从2.4.0
以来,稳定型有了很大的提升,很少在客户环境下崩溃。2.4.0
以前的版本,比如2.3.12
就常有客户反馈产品崩溃问题。产品质量的保证除了萌萌哒的QA的细心测试外,还有研发人员对提高单元测试覆盖率的努力。
目前PHP探针
daemon部分C0:65%,C1:51%
,当然在自动化测试这条路上,PHP探针含有很长的路要走。
TL;NR: 本文主要介绍PHP探针如何进行单元测试及在编写单测过程中遇到的问题和解决方法。
##框架选择
C语言(PHP探针使用C语言编写)单元测试框架有很多,但调研阶段只看过CUnit
的单测例子,如下所示,完整代码在这里:
int main()
{
CU_pSuite pSuite = NULL;
/* 初始化 */
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
pSuite = CU_add_suite("Suite_1", init_suite1, clean_suite1);
if (NULL == pSuite) {
CU_cleanup_registry();
return CU_get_error();
}
/* 注册单元测试函数 */
if ((NULL == CU_add_test(pSuite, "test of fprintf()", testFPRINTF)) ||
(NULL == CU_add_test(pSuite, "test of fread()", testFREAD)))
{
CU_cleanup_registry();
return CU_get_error();
}
/* 运行单测 */
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}
以前看到过java
的单测例子,只需要在函数头上添加一个@Test
的标签,JUnit
会自动执行该单测函数
而上面的CUnit
却还有很多冗余的工作要做:
* 写main函数
* 初始化,清理
* 注册编写的单测函数
第二个看到的C单测框架是Criterion,当时看到其主页上编写单测并运行的动画,感觉这个框架很不错,上面说的三个冗余操作都由框架完成,功能也挺丰富,就决定在项目中试用,本文使用的2.1.0
版本。
Criterion简介
支持如下功能:
* 测试函数自动注册
* 框架提供入口函数
* fork子进程,隔离测试
* 支持对程序exit码的测试
* 其他的。。。具体看github的readme
一个简单示例
#include <criterion/criterion.h>
Test(simple, test)
{
cr_assert(1);
}
编译执行
gcc -o test test.c -lcriterion && ./test
图片来自Criterion的github主页
##有关mock
- 以
glibc
中的socket
为例,一个简化的mock如下(需要动态链接glibc):
int (*real_socket)(int domain, int type, int protocol);
int socket(int domain, int type, int protocol)
{
if (mock) {
errno = EINVAL;
return -1;
}
if (!real_socket) {
real_socket = dlsym(RTLD_NEXT, "socket");
}
return real_socket(domain, type, protocol);
}
通过设置变量mock
决定是否使用。
2 PHP探针中需要跟服务器通信的部分,本文实现了一个简化的服务器线程,与单元测试运行在同一进程空间以方便测试。
PHP探针与单测代码集成编译
编写完文件A.c
的函数foo
的单测后,可以按以下方式编译:
1. 找出A.c中的所有符号依赖的所有其他源文件
2. 递归找出其他源文件依赖的源文.
3. 将所有源文件和单测文件编译链接成可执行文件
这种方法要对每个需要测试的文件都要找一遍依赖,比较繁琐。
使用ld
的--gc-sections
选项可以仅寻找被测函数foo的依赖,但也需要手动进行。
为了简化单测的编译链接过程,本文采用了如下方式:
1. 按编译可执行文件的方式编译项目中所有源文件,生成`.o`文件。
2. 使用`strip`从带`main`函数的`.o`文件中去掉`main`(Criterion自带入口函数)。
3. 将项目的`.o`与单测的`.o`链接成可执行文件。
调试出错的单测
由于使用该框架时还不支持单用例执行
,而且每个单测是在单独的子进程中,本文使用了一种比较笨的方法:
1. 在出错的单元测试函数开头添加kill(getpid(), SIGSTOP)
2. 编译运行,然后使用gdb -p <pid>跟踪调试
说明:设置set-follow-fork-mode
为child
也可以调试,但是需要知道出错的是第几个child才能设置。
好消息是最新的2.2.0版本,已经支持单个测试用例执行模式,调试就简单多了。
##写单元测试的一些感受
- PHP探针是在发了很多版后才开始写单测,补债的过程很痛苦;
- 写代码时要提高函数的可测试性:本文为PHP探针写单测时也是边重构边写单测,因为已有代码编写时没有考虑可测试性;
举几个写单侧过程中遇到的例子:
- 函数输入要能够从外部传入:
比如某个函数来自一个特定路径的文件,而该文件名是以宏的形式存在,这时这个函数就很难去测试
好的写法是将文件名以参数传入,这样就可以针对不同的文件内容做测试
2 有关static函数
static函数一般是内部函数,不在文件外访问,想要验证某个static函数bar就只能通过调用了bar的非static函数foo进行
但foo函数可能很难验证结果的正确性,比如:
static void bar(some_parameter *arg)
{
....
}
void foo()
{
some_parameter *arg;
arg = do_some_init();
bar(arg);
destroy_everything(arg);
return;
}
函数foo执行结束后销毁了内部状态,因此无法验证bar是否正确执行。
本文针对这类情况的做法是去掉static属性。
##Criterion自动注册单测函数的原理
Criterion对每种类型的单测都会在最终的ELF文件生成一个section
,如下以cr_
开头的段,设置编译器将单测函数放入指定的段,然后在运行时读取段中的内容。
localhost $ readelf -S unit_test
....
[28] cr_tst PROGBITS 00000000008abc60 002abc60
0000000000000048 0000000000000000 WA 0 0 8
[29] cr_sts PROGBITS 00000000008abca8 002abca8
0000000000000008 0000000000000000 WA 0 0 8
....