CPPUTest 虽然名称上看起来是 C++ 的单元测试框架, 其实它也是支持测试 C 代码的.
本文主要介绍用CPPUTest来测试 C 代码. (C++没用过, 平时主要用的是C) C++相关的内容都省略了.
本文基于 debian v7.6 x86_64.
1. CPPUTest 安装
现在各个Linux的发行版的源都有丰富的软件资源, 而且安装方便.
但是如果想要在第一时间使用最新版本的开源软件, 还是得从源码安装.
debian系统为了追求稳定性, apt源中的软件一般都比较旧. 所以本文中的例子是基于最新源码的CPPUTest.
1.1 apt-get 安装
$ sudo apt-get install cpputest
1.2 源码安装
1. 下载源码, 官网: http://cpputest.github.io/
2. 编译源码
$ tar zxvf cpputest-3.6.tar.gz $ cd cpputest-3.6/ $ ./configure $ make
最后我没有实际安装, 而是直接使用编译出的二进制。
2. CPPUTest 介绍
2.1 构造待测试代码 (C语言)
/* file: sample.h */ #include <stdio.h> #include <string.h> #include <stdlib.h> struct Student { char* name; int score; }; void ret_void(void); int ret_int(int, int); double ret_double(double, double); char* ret_pchar(char*, char*); struct Student* init_student(struct Student* s, char* name, int score);
/* file: sample.c */ #include "sample.h" #ifndef CPPUTEST int main(int argc, char *argv[]) { char* pa; char* pb; pa = (char*) malloc(sizeof(char) * 80); pb = (char*) malloc(sizeof(char) * 20); strcpy(pa, "abcdefg "); strcpy(pb, "hijklmn "); printf ("Sample Start...... "); ret_void(); printf ("ret_int: %d ", ret_int(100, 10)); printf ("ret_double: %.2f ", ret_double(100.0, 10.0)); printf ("ret_pchar: %s ", ret_pchar(pa, pb)); struct Student* s = (struct Student*) malloc(sizeof(struct Student)); s->name = (char*) malloc(sizeof(char) * 80); init_student(s, "test cpputest", 100); printf ("init_Student: name=%s, score=%d ", s->name, s->score); printf ("Sample End ...... "); free(pa); free(pb); free(s->name); free(s); return 0; } #endif void ret_void() { printf ("Hello CPPUTest! "); } /* ia + ib */ int ret_int(int ia, int ib) { return ia + ib; } /* da / db */ double ret_double(double da, double db) { return da / db; } /* pa = pa + pb */ char* ret_pchar(char* pa, char* pb) { return strcat(pa, pb); } /* s->name = name, s->score = score */ void init_student(struct Student* s, char* name, int score) { strcpy(s->name, name); s->score = score; }
2.2 测试用例的组成, 写法
CPPUTest 的测试用例非常简单, 首先定义一个 TEST_GROUP, 然后定义属于这个 TEST_GROUP 的 TEST.
需要注意的地方是:
1. 引用 CPPUTest 中的2个头文件
#include <CppUTest/CommandLineTestRunner.h>
#include <CppUTest/TestHarness.h>
2. 引用 C 头文件时, 需要使用 extern "C" {}
extern "C" { #include "sample.h" }
下面的例子是测试 sample.c 中 ret_int 的代码.
构造了一个测试成功, 一个测试失败的例子
/* file: test.c */ #include <CppUTest/CommandLineTestRunner.h> #include <CppUTest/TestHarness.h> extern "C" { #include "sample.h" } /* 定义个 TEST_GROUP, 名称为 sample */ TEST_GROUP(sample) {}; /* 定义一个属于 TEST_GROUP 的 TEST, 名称为 ret_int_success */ TEST(sample, ret_int_success) { int sum = ret_int(1, 2); CHECK_EQUAL(sum, 3); } /* 定义一个属于 TEST_GROUP 的 TEST, 名称为 ret_int_failed */ TEST(sample, ret_int_failed) { int sum = ret_int(1, 2); CHECK_EQUAL(sum, 4); } int main(int argc, char *argv[]) { CommandLineTestRunner::RunAllTests(argc, argv); return 0; }
2.3 测试用例结果判断 ( fail, 各种assert等等)
测试完成后, 可以用 CPPUTest 提供的宏来判断测试结果是否和预期一致.
CPPUTest 提供的用于判断的宏如下: (上面的测试代码就使用了 CHECK_EQUAL)
Assertion 宏 |
含义 |
CHECK(boolean condition) | condition==True则成功; 反之失败 |
CHECK_TEXT(boolean condition, text) | condition==True则成功; 反之失败, 并且失败时输出 text信息 |
CHECK_EQUAL(expected, actual) | expected==actual则成功; 反之失败 |
CHECK_THROWS(expected_exception, expression) | 抛出的异常 expected_exception==exception则成功; 反之失败 |
STRCMP_EQUAL(expected, actual) | 字符串 expected==actual则成功; 反之失败 |
LONGS_EQUAL(expected, actual) | 数字 expected==actual则成功; 反之失败 |
BYTES_EQUAL(expected, actual) | 数字 expected==actual则成功; 反之失败 (数字是 8bit 宽) |
POINTERS_EQUAL(expected, actual) | 指针 expected==actual则成功; 反之失败 |
DOUBLES_EQUAL(expected, actual, tolerance) | double型 expected和actual在误差范围内(tolerance)相等则成功; 反之失败 |
FAIL(text) | 总是失败, 并输出 text 信息 |
2.4 运行测试用例时的编译选项配置 (主要是C语言相关的)
这一步是最关键的, 也就是编译出单元测试文件. 下面是 makefile 的写法, 关键位置加了注释.
# makefile for sample cpputest CPPUTEST_HOME = /home/wangyubin/Downloads/cpputest-3.6 CC := gcc CFLAGS := -g -Wall CFLAGS += -std=c99 CFLAGS += -D CPPUTEST # 编译测试文件时, 忽略sample.c的main函数, sample.c的代码中用了宏CPPUTEST # CPPUTest 是C++写的, 所以用 g++ 来编译 测试文件 CPP := g++ CPPFLAGS := -g -Wall CPPFLAGS += -I$(CPPUTEST_HOME)/include LDFLAGS := -L$(CPPUTEST_HOME)/lib -lCppUTest sample: sample.o sample.o: sample.h sample.c $(CC) -c -o sample.o sample.c $(CFLAGS) # 追加的测试程序编译 test: test.o sample.o $(CPP) -o $@ test.o sample.o $(LDFLAGS) test.o: sample.h test.c $(CPP) -c -o test.o test.c $(CPPFLAGS) .PHONY: clean clean: @echo "clean..." rm -f test sample rm -f sample.o test.o
编译测试文件
make test <-- 会生成一个文件名为 test 可执行文件
编译sample程序时, 需要把 "CFLAGS += -D CPPUTEST" 这句注释掉, 否则没有main函数.
2.5 运行测试用例, 查看结果的方法
运行可执行文件 test 就可以实施测试.
$ ./test <-- 默认执行, 没有参数 test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ .. Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms) ================================================================================= $ ./test -c <-- -c 执行结果加上颜色 (成功绿色, 失败红色) test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ .. Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms) <-- bash中显示红色 ================================================================================= $ ./test -v <-- -v 显示更为详细的信息 TEST(sample, ret_int_failed) test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ - 1 ms TEST(sample, ret_int_success) - 0 ms Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms) ================================================================================= $ ./test -r 2 <-- -r 指定测试执行的次数, 这里把测试重复执行2遍 Test run 1 of 2 test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ .. Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 0 ms) Test run 2 of 2 test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ .. Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms) ================================================================================= $ ./test -g sample <-- -g 指定 TEST_GROUP, 本例其实只有一个 TEST_GROUP sample test.c:34: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ .. Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms) ================================================================================= $ ./test -n ret_int_success <-- -s 指定执行其中一个 TEST, 名称为 ret_int_success . OK (2 tests, 1 ran, 1 checks, 0 ignored, 1 filtered out, 0 ms) ================================================================================= $ ./test -v -n ret_int_success <-- 参数也可以搭配使用 TEST(sample, ret_int_success) - 0 ms OK (2 tests, 1 ran, 1 checks, 0 ignored, 1 filtered out, 0 ms)
2.6 补充: setup and teardown
上面 test.c 文件中 TEST_GROUP(sample) 中的代码是空的, 其实 CPPUTest 中内置了 2 个调用 setup 和 teardown.
在 TEST_GROUP 中实现这2个函数之后, 每个属于这个 TEST_GROUP 的 TEST 在执行之前都会调用 setup, 执行之后会调用 teardown.
修改 test.c 中的 TEST_GROUP 如下:
/* 定义个 TEST_GROUP, 名称为 sample */ TEST_GROUP(sample) { void setup() { printf ("测试开始...... "); } void teardown() { printf ("测试结束...... "); } };
重新执行测试: (每个测试之前, 之后都多了上面的打印信息)
$ make clean clean... rm -f test sample rm -f sample.o test.o $ make test g++ -c -o test.o test.c -g -Wall -I/home/wangyubin/Downloads/cpputest-3.6/include gcc -c -o sample.o sample.c -g -Wall -std=c99 -D CPPUTEST g++ -o test test.o sample.o -L/home/wangyubin/Downloads/cpputest-3.6/lib -lCppUTest $ ./test -v TEST(sample, ret_int_failed)测试开始...... test.c:44: error: Failure in TEST(sample, ret_int_failed) expected <3> but was <4> difference starts at position 0 at: < 4 > ^ 测试结束...... - 0 ms TEST(sample, ret_int_success)测试开始...... 测试结束...... - 0 ms Errors (1 failures, 2 tests, 2 ran, 2 checks, 0 ignored, 0 filtered out, 0 ms)
2.7 内存泄漏检测插件
内存泄漏一直是C/C++代码中令人头疼的问题, 还好, CPPUTest 中提供了检测内存泄漏的插件, 使用这个插件, 可使我们的代码更加健壮.
使用内存检测插件时, 测试代码 和 待测代码 在编译时都要引用.
-include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h
makefile 修改如下:
# makefile for sample cpputest CPPUTEST_HOME = /home/wangyubin/Downloads/cpputest-3.6 CC := gcc CFLAGS := -g -Wall CFLAGS += -std=c99 CFLAGS += -D CPPUTEST # 编译测试文件时, 忽略sample.c的main函数, sample.c的代码中用了宏CPPUTEST # CPPUTest 是C++写的, 所以用 g++ 来编译 测试文件 CPP := g++ CPPFLAGS := -g -Wall CPPFLAGS += -I$(CPPUTEST_HOME)/include LDFLAGS := -L$(CPPUTEST_HOME)/lib -lCppUTest # 内存泄露检测 MEMFLAGS = -include $(CPPUTEST_HOME)/include/CppUTest/MemoryLeakDetectorMallocMacros.h sample: sample.o sample.o: sample.h sample.c $(CC) -c -o sample.o sample.c $(CFLAGS) $(MEMFLAGS) # 追加的测试程序编译 test: test.o sample.o $(CPP) -o $@ test.o sample.o $(LDFLAGS) test.o: sample.h test.c $(CPP) -c -o test.o test.c $(CPPFLAGS) $(MEMFLAGS) .PHONY: clean clean: @echo "clean..." rm -f test sample rm -f sample.o test.o
修改 sample.c 中的 init_student 函数, 构造一个内存泄漏的例子.
/* s->name = name, s->score = score */ void init_student(struct Student* s, char* name, int score) { char* name2 = NULL; name2 = (char*) malloc(sizeof(char) * 80); /* 这里申请的内存, 最后没有释放 */ strcpy(s->name, name2); strcpy(s->name, name); s->score = score; }
修改 test.c 追加一个测试 init_student 函数的测试用例
TEST(sample, init_student) { struct Student *stu = NULL; stu = (struct Student*) malloc(sizeof(struct Student)); char name[80] = {'t', 'e', 's', 't', '