设为首页 加入收藏

TOP

linux环境编程(1): 实现一个单元测试框架(一)
2023-07-23 13:43:35 】 浏览:121
Tags:linux 单元测

写在前面

在开发的过程中,大多数人都需要对代码进行测试。目前对于c/c++项目,可以采用google的gtest框架,除此之外在github上搜索之后可以发现很多其他类似功能的项目。但把别人的轮子直接拿来用,终究比不过自己造一个同样功能的轮子更有成就感。作为“linux环境编程”系列文章的第一篇,本篇文章记录了如何用较少的代码实现一个可用的单元测试框架,这个测试框架将一直在后续系列文章中的代码实例环节使用,并且在使用过程中不断完善和改进。相比于文字说明,我相信有人更喜欢直接看代码实现,所以这里先放一个github传送门

需求来源

先给项目取个名字吧,毕竟命名是最困难的环节了:P. 这个测试框架一句话可以概括为"a simple unit test framework for c programming language", 所以就叫它"cutest"好了,希望不要有人把前缀"cu"和"cuda"联系起来。

接下来我把自己切换到用户的角度,说说我希望自己如何使用cutest。首先,测试的对象对我来说是一个个独立的函数,我希望框架提供一种让我定义单元测试函数的方法,定义完成之后我作为用户的任务就完成了,至于如何让框架知道有一个新的单元测试加入进来了,那不是我这个用户应该关心的,我甚至不打算写main函数,然后在main函数里告诉cutest去运行所有注册进去的测试。我想要的只是定义单元测试,编译链接,运行得到结果。总结起来,需求有以下3点:

  1. 用户只要定义自己的单元测试函数即可。单元测试集的管理,运行,不需要用户额外编写代码,即只要用户定义完单元测试函数,编译之后这个单元测试就自动被测试框架接管了,框架不需要提供额外的单元测试注册接口。
  2. 在单元测试函数中支持断言。类似gtest的EXCEPT_EQ等宏的功能。
  3. 不需要其他外部依赖。用户只要有一个框架的头文件和编译好的库文件就可以直接使用。

在说明实现原理之前,我先剧透一下最终的用户是如何使用cutest编写单元测试的:

/*test1.c*/

/*include cutest header file*/
#include "cutest.h"

/*define a new test suit with the name 'test_suit1'*/
CUTEST_SUIT(test_suit1)

/*define first test case in test_suit1*/
CUTEST_CASE(test_suit1, test1) {
    int a = 10;
    int b = 10;
    CUT_EXPECT_EQ(a, b);
}

/*define the second test case in test_suit1*/
CUTEST_CASE(test_suit1, test2) {
    int a = 10;
    int b = 20;
    CUT_EXPECT_GT(a, b);
}

编译这个test1.c文件,然后在shell中运行就可以得到结果了:

$ ./test1
 cutest summary:
         [test_suit1] suit result: 1/2
         [test_suit1::test2] case result: Fail
         [test_suit1::test1] case result: Pass

用户的的单元测试可以放在多个文件中,但是定义单元测试函数的步骤是不变的,用户的所有测试都会在运行结束之后得到一个汇总的统计信息。可以看到,cutest使用起来还是很简单方便的。

如何实现

聊完了用户的想法,需要考虑如何实现了。

基本数据结构

在cutest中我定义了两种重要的数据结构:

  • test_case:

    最基本的单元测试。对应用户实现的单元测试函数,test_case中还记录了测试的名字,测试的最终运行结果。

  • test_suit:

    单元测试集合。用于管理一组test_case,理论上应该把一组相关性较大的test_case放在同一个集合中。测试框架中可以同时存在多个test_suit。

为了管理用户定义的多个单元测试, 我采用了链表数据结构。test_case本身是一个链表,通过当前的test_case可以找到下一个test_case;每个test_suit中包含一个test_case的链表,这个链表就是这个test_suit管理的全部单元测试,test_suit自身也是一个链表,通过这个链表指针可以找到下一个test_suit。两个结构体的定义如下:

typedef struct test_case {
    char *test_name;
    test_func test_func;
    struct test_case *test_case_next;
    int test_result;
} test_case;

typedef struct test_suit {
    char *test_suit_name;
    test_case *test_case_list;
    int test_case_total;
    int test_case_passed;
    struct test_suit *test_suit_next;
} test_suit;

在test_case中除了维护链表,还记录了测试名字和结果;同理,test_suit额外记录了当前测试集合的名字,包含的全部单元测试个数,以及测试通过的个数,这些信息将用于最后的测试结果统计。

如何实现单元测试的自动注册

为了管理全部的单元测试,还需要一个test_suit的头节点,当用户代码定义了新的test_suit的时候,会自动的加入到这个头节点上;同时,当用户在新的test_suit上定义新的test_case时,需要能够自动加入到该test_suit的test_case链表上。下面的一个关键问题就是如何在用户定义test_suit或者test_case的同时自动注册到cutest框架中。这里用到的方式是通过编译器给一个函数添加"construtor"属性,这样在程序运行时,具有"constructor"属性的函数会由c库调用,编程者可以定义多个"constructor"函数,这些函数会在main函数开始之前被调用,利用这个特性我们可以在“constructor”函数中实现test_suit以及test_case的注册,这样就实现了让cutest框架管理这些用户数据。下面以CUTEST_SUIT宏的实现为例,说明test_suit的注册是如何完成的:

/* CUTEST_SUIT宏的功能由两个辅助宏实现.
 * __DEFINE_CUTEST_SUIT定义个一个test_suit变量,并对其成员进行了初始化;
 * __REGISTER_CUTEST_SUIT定义了一个"constructor"函数, 在该函数中, 上一步定义的test_suit被添加到了test_suit的链表中;
 */
#define CUTEST_SUIT(suit_name)                                                 \
    __DEFINE_CUTEST_SUIT(suit_name)                                            \
    __REGISTER_CUTEST_SUIT(suit_na
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Ansible介绍以及基于角色搭建LNMP.. 下一篇Linux连接MobaXterm出现的问题及..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目