CPPUTest 单元测试框架(针对 C 单元测试的以验证)

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\0");
    strcpy(pb, "hijklmn\0");

    printf ("Sample Start......\n");

    ret_void();
    printf ("ret_int: %d\n", ret_int(100, 10));
    printf ("ret_double: %.2f\n", ret_double(100.0, 10.0));
    printf ("ret_pchar: %s\n", 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\n", s->name, s->score);
    printf ("Sample End  ......\n");
    free(pa);
    free(pb);
    free(s->name);
    free(s);

    return 0;
}
#endif

void ret_void()
{
    printf ("Hello CPPUTest!\n");
}

/* 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
    #include

 

  1. 引用 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 ("测试开始......\n");
    }

    void teardown()
    {
        printf ("测试结束......\n");
    }
};

 

再履行测试: (每个测试之前, 之后还多矣端的打印信息)

$ 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', '\0'};

    init_student(stu, name, 100);
    free(stu);
}

 

实践测试, 可以窥见测试结果受到唤醒 sample.c 72 行有内存泄漏风险,

随即一行正是 init_student 函数中因故 malloc 申请内存的那一行.

$ 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  -include /home/wangyubin/Downloads/cpputest-3.6/include/CppUTest/MemoryLeakDetectorMallocMacros.h
gcc -c -o sample.o sample.c -g -Wall -std=c99 -D CPPUTEST             -include /home/wangyubin/Downloads/cpputest-3.6/include/CppUTest/MemoryLeakDetectorMallocMacros.h
g++ -o test test.o sample.o -L/home/wangyubin/Downloads/cpputest-3.6/lib -lCppUTest
$ ./test -v -n init_student
TEST(sample, init_student)测试开始......
测试结束......

test.c:47: error: Failure in TEST(sample, init_student)
    Memory leak(s) found.
Alloc num (4) Leak size: 80 Allocated at: sample.c and line: 72. Type: "malloc"
     Memory: <0x120c5f0> Content: ""
Total number of leaks:  1
NOTE:
    Memory leak reports about malloc and free can be caused by allocating using the cpputest version of malloc,
    but deallocate using the standard free.
    If this is the case, check whether your malloc/free replacements are working (#define malloc cpputest_malloc etc).


 - 0 ms

Errors (1 failures, 3 tests, 1 ran, 0 checks, 0 ignored, 2 filtered out, 0 ms)