文章目录
本文为对《CUnit Programmer Guide》翻译,加入了一些自己的理解和重新排版,省略了一些表格,链接为英文原版的链接:
###简介
CUnit是一个用于在C中编写,管理和运行单元测试的系统,它被构建为一个与用户测试代码链接的静态库。CUnit是一个平台无关框架与各种用户界面的组合。核心框架为管理测试注册表,套件和测试用例提供了基本支持。用户界面便于与框架交互以运行测试和查看结果。
CUnit的框架组织方式类似于传统的单元测试框架:
主要分为三个级别,所有的测试用例分别放到不同的测试包(suite)里,而这些测试包由放到同一个测试注册表(registry)中。注册表中的所有包/测试都可以使用单个函数调用运行,或可以运行所有的测试。
CUnit框架使用的典型步骤如下:
###编写CUnit测试用例
####测试函数
除了不应该修改CUnit框架内的函数外,CUnit对测试函数的内容是没有限制的,同时,测试函数可以调用其他函数。下面是返回两个int型数值中最大值的函数测试用例:
int maxi(int i1, int i2)
{
return (i1 > i2) ? i1 : i2;
}
void test_maxi(void)
{
CU_ASSERT(maxi(0,2) == 2);
CU_ASSERT(maxi(0,-2) == 0);
CU_ASSERT(maxi(2,2) == 2);
}
####CUnit断言
CUnit提供了一组测试逻辑条件的断言。这些断言的成功或失败由框架跟踪,并且可以在测试运行完成时查看。每个断言只能测试一个逻辑条件,失败则为FALSE。失败时,除非用户选择了 xxx_FATAL
版本的断言,否则测试程序不会结束。如果用户选择了 xxx_FATAL
版本的断言,测试程序(函数)会立即返回,测试程序将没有机会清除注册表。所以,FATAL版本的断言需要谨慎使用。
还有一些特殊的“断言”并不是用来检测逻辑条件,这些用于测试控制流程或其他不需要逻辑测试的条件:
void test_longjmp(void)
{
jmp_buf buf;
int i;
i = setjmp(buf);
if (i == 0) {
run_other_func();
CU_PASS("run_other_func() succeeded.");
}
else
CU_FAIL("run_other_func() issued longjmp.");
}
测试函数调用的其他函数也可以自由的使用CUnit断言,同样也可以使用FATAL的版本,如果在这样的断言处失败,测试函数(整个调用链)将会被打断。
###测试注册表(registry)
/*****************************
- 内部结构体
- 测试注册表是测试套件的仓库,CUnit维护一个活动的测试注册表,用户在添加套件或者测试时会自动更新这个注册表。
*****************************/
typedef struct CU_TestRegistry
{
unsigned int uiNumberOfSuites; //< 存储在注册表中的所有套件数
unsigned int uiNumberOfTests; //< 所有测试数
CU_pSuite pSuite; //< 指向注册套件的链表头的指针
} CU_TestRegistry;
typedef CU_TestRegistry* CU_pTestRegistry
/*****************************
- 初始化注册表,一个测试注册表在使用之前必须被初始化,这个函数必须在所有的测试函数调用之前调用
- 返回值可能有两个
- CUE_SUCCESS 初始化成功
- CUE_NOMEMORY 初始化时内存申请失败
*****************************/
CU_ErrorCode CU_initialize_registry(void)
/*****************************
- 测试结束时,用户需要调用此函数以清除并释放框架使用的内存,这个函数框架中最后一个被调用的函数(与上面那个函数是成对出现的)
- 该函数如果未调用或者执行失败可能会造成内存泄漏,所以可以调用多次确保不会失败。同时要注意的是,这个函数一旦调用,注册表里的所有套件以及测试都将被销毁,所以指向这些套件或者测试的指针在这之后也不能再引用。
*****************************/
void CU_cleanup_registry(void)
/*****************************
- 返回指向测试注册表的指针
- CUnit并不推荐使用该接口对内部的注册表进行直接的操作,推荐使用API来操作
*****************************/
CU_pTestRegistry CU_get_registry(void)
/*****************************
- 用一个测试注册表去替换当前活动的测试注册表,而返回的是之前活动的注册表的指针。用户需要自己去销毁原来的那个注册表(CU_destroy_existing_registry)。应该注意的是不要显示的销毁当前活动的注册表。
*****************************/
CU_pTestRegistry CU_set_registry(CU_pTestRegistry pTestRegistry)
/*****************************
- 创建一个新注册表,返回一个指向它的指针。这个新的注册表将不包含任何的套件和测试,同样,它的销毁将由用户自己负责。
*****************************/
CU_pTestRegistry CU_create_new_registry(void)
/*****************************
- 销毁一个指定的注册表并释放所有相关的内存
- 当前活动的注册表不能用该函数进行销毁,当前活动的注册表将由CU_cleanup_registry来销毁
*****************************/
void CU_destroy_existing_registry(CU_pTestRegistry* ppRegistry)
###管理套件和测试(suite & test)
typedef struct CU_Suite
typedef CU_Suite* CU_pSuite
typedef struct CU_Test
typedef CU_Test* CU_pTest
/*****************************
- 三个函数指针
*****************************/
typedef void (*CU_TestFunc)(void)
typedef int (*CU_InitializeFunc)(void)
typedef int (*CU_CleanupFunc)(void)
/*****************************
- 向注册表中添加套件
- 创建一个新的测试套件(suite),该套件有一个strName作为标志,同时有自己的初始化函数和清除函数。新的套件向注册表注册并被其拥有,所以注册表必须在添加套件之前就已经初始化完成。(当前并不支持创建独立于注册表以外的套件)
- 套件的名称必须是唯一的,而初始化和清除函数则是可选的,并作为函数指针传递到此函数中。这些函数不带参数,如果成功则返回0,否则为非0。如果一个套件不需要这两个函数中的一个或两个,则传递NULL即可。
- 函数返回一个指向新建的套件的指针,这个指针在添加测试时会被用到。如果新建过程中出现错误,将返回NULL。框架的ERROR_CODE也会被设为以下的某一个值:
CUE_SUCCESS 新建成功
CUE_NOREGISTRY 没有被初始化的注册表
CUE_NO_SUITENAME 没有指定套件名
CUE_DUP_SUITE 套件的名称不唯一
CUE_NOMENORY 内存分配失败
*****************************/
CU_pSuite CU_add_suite(const char* strName,
CU_InitializeFunc pInit,
CU_CleanupFunc pClean);
/*****************************
- 将测试添加到套件中
- 同样是指定一个特定的名字strName(这个测试名只需要不同于该套件里的其他测试即可),不同的是测试函数不能为NULL,同时需要制定一个添加的目标套件。
- 传入的函数指针指向的函数既没有传参也没有返回值。
- 返回值是一个指向新建的测试的指针,如果在新建过程中出现错误,将返回NULL。同样,会给框架的ERROR_CODE设定一些特定的值
CUE_SUCCESS 新建测试成功
CUE_NOSUITE 没有指定合法的套件
CUE_NO_TESTNAME 没有指定测试名
CUE_NO_TEST 没有指定测试函数名
CUE_DUP_TEST 测试名不唯一
CUE_NOMEMORY 内存分配失败
*****************************/
CU_pTest CU_add_test(CU_pSuite pSuite,
const char* strName,
CU_TestFunc pTestFunc);
//< 这个宏的意义很明显,将测试函数名直接作为测试名,并将测试加入到指定的suite中去
#define CU_ADD_TEST(suite, test) \
( CU_add_test(suite, #test, (CU_TestFunc)test) )
对于包含很多测试和套件的大型测试结构,测试的管理、套件的关联即注册等冗长且很容易出错。CUnit提供了一个特殊的注册系统来帮助管理套件和测试。它的主要优点在于集中的注册套件和相关测试,并最大限度的减少用户需要写的错误检查代码。
typedef struct CU_TestInfo
typedef struct CU_SuiteInfo
/*****************************
- 每个数组元素包含一个唯一的测试名和一个函数函数,数组必须要争有一个NULL值为元素结束,包含在单个数组中的测试用例将会被注册到单个测试套件中去。
*****************************/
CU_TestInfo test_array1 [] = {
{"testname1",test_func1},
{"testname2",test_func2},
{"testname3",test_func3},
CU_TEST_INFO_NULL,
};
CU_SuiteInfo suites[] = {
{ "suitename1", suite1_init-func, suite1_cleanup_func, test_array1 },
{ "suitename2", suite2_init-func, suite2_cleanup_func, test_array2 },
CU_SUITE_INFO_NULL,
};
/*****************************
- 第一个函数可直接将上面定义的套件数组注册到注册表中去,如果中间任何一个套件或者测试注册错误,都会返回一个错误码
- 第二个函数可以注册多个suite的数组
*****************************/
CU_ErrorCode CU_register_suites(CU_SuiteInfo suite_info[]);
CU_ErrorCode CU_register_nsuites(int suite_count, ...);
//< e.g.:
CU_ErrorCode error = CU_register_nsuites(2, suites1, suites2);
###执行测试
下面所有的模式在官方的实例中均有截图实例来说明具体的操作,链接如下,这里我只介绍API
####自动模式
/*****************************
- 自动模式
- 自动化界面是非交互式的,测试启动后结果会输出到XML文件中,住处的测试和套件的列表也可以记录到XML文件中。
*****************************/
#include <CUnit/Automated.h>
/*
- 执行所有注册了的套件,执行结果将输出到一个名为ROOT-Reslut.xml文件中
- ROOT(文件名)可以用CU_set_output_filename函数来设置,否则使用默认的文件名:CUnitAutomated-Result.xml
- Notice: 如果每次运行之前不设置目标文件名ROOT,则结果文件将被覆盖
*/
void CU_automated_run_tests(void);
/*
将注册的套件和测试也输出到XML文件里
*/
CU_ErrorCode CU_list_tests_to_file(void);
/*
设置结果输出的文件名,输入的文件名加上-Result.xml为结果文件,加上-Listing.xml为套件和测试列表的文件
*/
void CU_set_output_filename(const char* szFilenameRoot);
####基本模式
/*****************************
- 基本模式
- 基本模式同样是非交互式的,测试输出到标准输出中(stdout),这个模式支持执行单独的套件或测试,并且支持由客户端来控制每次执行的输出类型。这个模式为希望简化访问CUnit API的客户端提供了最大的灵活性
*****************************/
#include <CUnit/Basic.h>
typedef enum CU_BasicRunMode
/*
在所有注册套件中运行所有测试,返回测试运行期间发生的第一个错误代码,输出类型由当前的运行模式控制,这个模式可以有CU_basic_set_mode来设置。
*/
CU_ErrorCode CU_basic_run_tests(void)
/*
执行指定套件中的所有测试,同样返回测试中发生的第一个错误的代码
*/
CU_ErrorCode CU_basic_run_suite(CU_pSuite pSuite)
/*
执行特定套件中的特定测试,同样返回测试中发生的第一个错误的代码
*/
CU_ErrorCode CU_basic_run_test(CU_pSuite pSuite, CU_pTest pTest)
/*
设置基本模式的运行模式,控制输出的类型
CU_BRM_NROMAL 失败和执行的总结会打印出来
CU_BRM_SILENT 错误信息以外的输出都不会打印出来
CU_BRM_VERBOSE 打印所有可能输出的细节
*/
void CU_basic_set_mode(CU_BasicRunMode mode)
/*
返回当前的运行模式
*/
CU_BasicRunMode CU_basic_get_mode(void)
/*
将所有的失败信息打印到stdout中,不依赖于运行模式!
*/
void CU_basic_show_failures(CU_pFailureRecord pFailure)
####交互式控制台模式
/*****************************
- 控制台模式
- 控制台模式是可交互的,被测试程序(客户端)只需要启动控制台会话,然后用户以交互方式来控制测试的进行。
- 操作包括选择和运行注册的套件和测试,查看测试结果
*****************************/
#include <CUnit/Console.h>
void CU_console_run_tests(void)
####交互式Curse模式
/*****************************
- 交互式Curses模式(与控制台模式很类似)
- 同样是可交互的,被测试程序(客户端)只需要启动Curses界面会话,然后用户以交互方式来控制测试的进行。
- 操作包括选择和运行注册的套件和测试,查看测试结果
*****************************/
#include <CUnit/CUCurses.h>
void CU_curses_run_tests(void)
####获取测试结果
/*****************************
- 客户端代码有时候需要直接访问测试的结果,包括测试执行中的各种计数以及一个保存测试失败的细节的链表!
- 每次启动新的测试或者是在初始化、清除出侧标时,测试结果都会被覆盖!
*****************************/
#include <CUnit/TestRun.h> //< (included automatically by <CUnit/CUnit.h>)
/*
以下若干接口可用来报告套件的数量、测试的数量、失败或者通过的断言数量。
*/
unsigned int CU_get_number_of_suites_run(void)
unsigned int CU_get_number_of_suites_failed(void)
unsigned int CU_get_number_of_tests_run(void)
unsigned int CU_get_number_of_tests_failed(void)
unsigned int CU_get_number_of_asserts(void)
unsigned int CU_get_number_of_successes(void)
unsigned int CU_get_number_of_failures(void)
/*
一次获取所有的运行数据,所有上面的运行数据保存在同一个结构体里
*/
typedef struct CU_RunSummary
{
unsigned int nSuitesRun;
unsigned int nSuitesFailed;
unsigned int nTestsRun;
unsigned int nTestsFailed;
unsigned int nAsserts;
unsigned int nAssertsFailed;
unsigned int nFailureRecords;
} CU_RunSummary;
typedef CU_Runsummary* CU_pRunSummary
const CU_pRunSummary CU_get_run_summary(void)
/*
获取测试中所有失败的一个链表,每一个失败都包含一些定位信息和状态信息。
*/
typedef struct CU_FailureRecord
{
unsigned int uiLineNumber;
char* strFileName;
char* strCondition;
CU_pTest pTest;
CU_pSuite pSuite;
struct CU_FailureRecord* pNext;
struct CU_FailureRecord* pPrev;
} CU_FailureRecord;
typedef CU_FailureRecord* CU_pFailureRecord
const CU_pFailureRecord CU_get_failure_list(void)
/*
获取测试中保存失败的链表的长度(失败数量)
Notice: 这个数量可能大于测试失败的数量,因为像套件初始化失败等等错误也会记录在里面!
*/
unsigned int CU_get_number_of_failure_records(void)
###异常处理
#include <CUnit/CUError.h> //< (included automatically by <CUnit/CUnit.h>)
typedef enum CU_ErrorCode
/*****************************
- 大多数的CUnit函数都可以设置一个error_code来标志框架的异常状态。有些函数直接返回这个异常码,而另外的会设置这个code(返回其他信息)。以下两个函数用来获取当前框架的异常状态。
- 第一个返回异常值
- 第二个返回异常的描述
- 所有的异常值和描述不列出,可参考手册原文
*****************************/
CU_ErrorCode CU_get_error(void);
const char* CU_get_error_msg(void);
typedef enum CU_ErrorAction
/*****************************
- 遇到异常时,默认的行为是设置ERROR_CODE后继续执行,而有些测试情况下需要测试停止在框架错误处,或者测试应用程序直接退出。这个异常发生后的行为可由用户设置。
- 可设置为以下值
CUEA_IGNORE(default) 程序继续运行
CUEA_FAIL 程序运行停止
CUEA_ABORT 程序退出
*****************************/
void CU_set_error_action(CU_ErrorAction action);
CU_ErrorAction CU_get_error_action(void);
###官方实例解析
CUnit官方提供了一个简单的实例,下面这个解析对该实例中注释做了删减,加上了中文注释,如果想看原版,可直接访问以下链接:
#include <stdio.h>
#include <string.h>
#include "CUnit/Basic.h"
/* 新建FILE指针指向测试用的文件,暂时初始化为空,等待打开文件 */
static FILE* temp_file = NULL;
/*
- suite的初始化函数,根据suite初始化函数的要求,返回0则初始化成功,否则失败
- suite初始化的动作是打开 temp.txt 文件
*/
int init_suite1(void)
{
if (NULL == (temp_file = fopen("temp.txt", "w+"))) {
return -1;
}
else {
return 0;
}
}
/*
- suite的清除函数,与初始化函数相同的要求,返回0为成功,否则失败
- suite的清除则为关闭 temp.txt 文件
*/
int clean_suite1(void)
{
if (0 != fclose(temp_file)) {
return -1;
}
else {
temp_file = NULL;
return 0;
}
}
/*
测试函数,测试fprintf返回的字节数是否正确
*/
void testFPRINTF(void)
{
int i1 = 10;
if (NULL != temp_file) {
CU_ASSERT(0 == fprintf(temp_file, ""));
CU_ASSERT(2 == fprintf(temp_file, "Q\n"));
CU_ASSERT(7 == fprintf(temp_file, "i1 = %d", i1));
}
}
/*
- 测试fread()
- 这个测试函数一定要在testFPRINTF()函数之后,否则测试就会失败
*/
void testFREAD(void)
{
unsigned char buffer[20];
if (NULL != temp_file) {
rewind(temp_file); //< 当前文件流的位置重新设置为文件开头
CU_ASSERT(9 == fread(buffer, sizeof(unsigned char), 20, temp_file));
CU_ASSERT(0 == strncmp(buffer, "Q\ni1 = 10", 9));
}
}
/*
main 函数,执行测试的全过程
*/
int main()
{
CU_pSuite pSuite = NULL;
//< step1 测试注册表初始化
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
//< step2 给测试注册表添加一个套件(suite)
//< 该函数返回添加的套件指针,若为空则创建失败,返回错误
pSuite = CU_add_suite("Suite_1", init_suite1, clean_suite1);
if (NULL == pSuite) {
CU_cleanup_registry();
return CU_get_error();
}
//< step3 给套件添加两个测试,且必须是先添加testFPRINTF,再添加testFREAD
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();
}
//< step4 用基本模式执行所有的测试
CU_basic_set_mode(CU_BRM_VERBOSE); //< 设定了基本模式的输出模式(输出所有可能的细节)
CU_basic_run_tests();
//< step5 清除注册表(所有测试和套件)
CU_cleanup_registry();
return CU_get_error();
}
//< 测试结果如下
/*
* Suite: Suite_1
* Test: test of fprintf() ... passed
* Test: test of fread() ... passed
*
* --Run Summary: Type Total Ran Passed Failed
* suites 1 1 n/a 0
* tests 2 2 2 0
* asserts 5 5 5 0
*/