标签 单元测试 下的文章

C单元测试包设计与实现

在Java、C++和C#等高级语言的单元测试正进行的如火如荼的时候,C好像做了看客,冷清的躲在了一个不起眼的角落里。C并不是没有单元测试工具,像Check和CUnit这样的工具也很有名气,只是和大名鼎鼎的JUnit比起来,还是显得有些英雄气短。很多大型的C项目,如APR等都没有使用像Check、CUnit这样通用的单元测试框架,而是另起炉灶自己编写。其实编写一个仅能满足单个项目需要的C单元测试工具包并非难事。在部分参考APR的ABTS的前提下,我们也来设计一套自己的简单的C语言单元测试包。

鉴于减少复杂性,我们的目标仅仅是设计和实现一套能在单进程、单线程下工作良好的C单元测试包,我们暂且将之命名为CUT – C Unit test Toolkit。
1、CUT涉及的术语解释
曾经接触过多个有名的单元测试框架如JUnit、CppUnit、TestNG等,它们在对单元测试某些概念的理解上并不是全都一样的。这里我们也有我们自己的定义。
a) 一个逻辑unit test包含至少一个或者多个suite;
b) 一个suite包含至少0或者多个test case;
c) 每个test case中至少包含1个或者多个“断言类”语句。

2、CUT预告片
其实每设计一个程序之前自己都会考虑该提供给用户怎样的东东呢?下面是应该是CUT的经典用法:
cut_ts_t *suite = NULL;

CUT_TEST_BEGIN("classic usage of CUT");

CUT_TS_INIT(suite); 
CUT_TC_ADD(suite, "test case: tc_add", tc_add);
CUT_TC_ADD(suite, "test case: tc_sub", tc_sub);
CUT_TS_ADD(suite, my_setup, my_teardown);

CUT_TEST_RUN();

CUT_TEST_REPORT();

CUT_TEST_END();

3、CUT的组织结构
从上面的经典用法中也可以看出我们的CUT的组织是这样的:
            Test
             |
             |
        +————-+
       TS-1    …  TS-N
        |                |
        |                |
   +——-+ …   +——–+
  TC-1   TC-N     TC-1     TC-N
其中:TS – Test Suite,TC – Test Case

4、CUT接口设计与实现
在“预告片”中我们已经暴露了大部分CUT的重要接口,在下面我们将伴随着实现逐一说明。另外在CUT的实现中我们使用了APR RING技术,不了解APR RING的可以参见我的上一篇Blog“APR分析-环篇”。
1) 主要数据结构
typedef void (*tc_func)(cut_tc_t *tc); /* Test Case标准原型函数指针,所有的Test Case都应该符合这个原型 */
typedef void (*fixture_func)();  /* 用于suite环境建立和拆除的func原型 */

/* Test Case数据结构 */
typedef struct cut_tc_t {
        APR_RING_ENTRY(cut_tc_t)        link;
        char                            name[CUT_MAX_STR_LEN+1];
        tc_func                         func;
        int                             failed;
} cut_tc_t;
typedef APR_RING_HEAD(cut_tc_head_t, cut_tc_t) cut_tc_head_t;

/* Test Suite数据结构 */
typedef struct cut_ts_t {
        APR_RING_ENTRY(cut_ts_t)        link;
        cut_tc_head_t                   tc_head;
        int                             failed; /* 失败用例总数 */
        int                             ran; /* 运行用例总数 */
        fixture_func                    sf; /* setup func */
        fixture_func                    tf; /* teardown func */
} cut_ts_t;
typedef APR_RING_HEAD(cut_ts_head_t, cut_ts_t) cut_ts_head_t;

/* 逻辑单元测试数据结构 */
typedef struct cut_test_t {
        char                            name[CUT_MAX_STR_LEN+1];
        cut_ts_head_t                   ts_head;
} cut_test_t;

2) CUT_TEST_BEGIN和CUT_TEST_END
这两者分别是一个逻辑Test的开始与结束。我们在CUT_TEST_BEGIN建立好我们的内部数据结构,其唯一宏参数用来加强可读性,在CUT_TEST_END中释放在测试过程中获取的系统资源。其实现如下:
#define CUT_TEST_BEGIN(desc) \
        cut_test_t *_cut_test = NULL; \
        _cut_test = malloc(sizeof(cut_test_t)); \
        if (_cut_test == NULL) { \
                return errno; \
        } \
        memset(_cut_test, 0, sizeof(cut_test_t)); \
        APR_RING_INIT(&(_cut_test->ts_head), cut_ts_t, link); \
        strncpy(_cut_test->name, desc, CUT_MAX_STR_LEN)

#define CUT_TEST_END() do { \
  /* 这里遍历Ring,释放其他相关内存,这里限于篇幅未写出 */
                if (_cut_test != NULL) { \
                        free(_cut_test); \
                } \
        } while(0)

3) CUT_TS_ADD和CUT_TC_ADD
前者负责向一逻辑单元测试中添加Test Suite,后者则负责向一个Test Suite中添加测试用例。在CUT中,每个Test Suite依赖两个Fixture Function- setup和teardown。setup用于建立测试环境,比如打开某文件,获得文件句柄供该Test Suite中的若干Test Case使用;而teardown则用来做后处理,释放setup以及在众多Test Case执行时分配的资源,比如上面关闭提到的文件句柄。

在实现CUT的Test Suite时,实际上加了一个对用户使用的限制,那就是CUT负责管理Test Suite的内存分配,说限制也好我觉得倒是给用户提供了一种方便。这两个宏的实现如下:
#define CUT_TEST_SUITE_INIT(suite) do { \
                if (suite == NULL) { \
                        suite = malloc(sizeof(cut_ts_t)); \
                        if (suite == NULL) { \
                                return errno; \
                        } \
                } \
                memset(suite, 0, sizeof(cut_ts_t)); \
                APR_RING_INIT(&(suite->tc_head), cut_tc_t, link); \
                suite->ran = 0; \
                suite->failed = 0; \
        } while(0)

#define CUT_TS_ADD(suite, f1, f2) do { \
                APR_RING_ELEM_INIT(suite, link); \
                suite->sf = f1; \
                suite->tf = f2; \
                APR_RING_INSERT_TAIL(&(_cut_test->ts_head), suite, cut_ts_t, link); \               
        } while(0)

#define CUT_TC_ADD(suite, desc, f1) do { \
                cut_tc_t *tc = NULL; \
                tc = malloc(sizeof(cut_tc_t)); \
                if (tc == NULL) { \
                        return errno; \
                } \
                memset(tc, 0, sizeof(cut_tc_t)); \
                strncpy(tc->name, desc, CUT_MAX_STR_LEN); \
                tc->func = f1; \
                APR_RING_ELEM_INIT(tc, link); \
                APR_RING_INSERT_TAIL(&(suite->tc_head), tc, cut_tc_t, link); \
        } while(0)

4) CUT_TEST_RUN和CUT_TEST_REPORT
这两个宏的作用分别是运行所有逻辑单元测试中的测试用例和报告测试情况,在这里CUT_TEST_REPORT输出形式较为简单,只是打印出此次单元测试运行用例总数和失败的用例数。当然要丰富其输出形式,让用户更快更早定位哪个测试用例失败也并不难,只需对CUT的实现稍作修改即可,这里仅是抛砖引玉。具体可参见成熟的工具的输出形式,如CUnit等。
#define CUT_TEST_RUN() do { \
                cut_ts_t *ts = NULL; \
                cut_tc_t *tc = NULL; \
                APR_RING_TRAVERSE(ts, &(_cut_test->ts_head), cut_ts_t, link) { \
                        if (ts != NULL) { \
                                if (ts->sf != NULL) { \
                                        ts->sf(); \ /* execute setup func */
                                } \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                                tc->func(tc); \
                                        } \
                                } \
                                if (ts->tf != NULL) { \
                                        ts->tf(); \ /* execute teardown func */
                                } \
                        } \
                } \
        } while(0)

#define CUT_TEST_REPORT() do { \
                int ran = 0; \
                int failed = 0; \
                cut_ts_t *ts = NULL; \
                cut_tc_t *tc = NULL; \
                APR_RING_TRAVERSE(ts, &(_cut_test->ts_head), cut_ts_t, link) { \
                        if (ts != NULL) { \
                                APR_RING_TRAVERSE(tc, &(ts->tc_head), cut_tc_t, link) { \
                                        if (tc != NULL) { \
                                                ran++; \
                                                failed += tc->failed; \
                                        } \
                                } \
                        } \
                } \
                printf("total tc is %d, and failed tc is %d\n", ran, failed); \
        } while(0)

5) 断言集合
评价一个单元测试工具好坏的重要标准之一就是它的断言集的多寡和易用性。大部分单元测试工具都提供几十个各种各样的断言接口,我这里仅仅是举一个断言接口例子:
我们提供一个整型数判等断言接口:
void cut_int_equal(cut_case_t *tc, const int expected, const int actual, int lineno)
{
        if (expected != actual) {
                tc->failed += 1;
  /* 其他处理,如记录断言发生位置信息等 */
        }
}

这样我们就可以在我们的测试用例中这样使用了:
void tc_add(cut_case_t *tc) {
        int a = 1;
        int b = 2;
        cut_int_equal(tc, 3, add(a, b), __LINE__);
}

5、一个简单但完整的测试实例
CUT以头文件和静态库的形式发布,使用CUT只需要引用其头文件,并在链接时链接CUT的静态库即可。
在下面的例子中我们执行了两个测试用例:
#include "cut.h"
#include "my_math.h" //for add and sub interface

void my_setup() {
        printf("setup for suite\n");
}

void my_teardown() {
        printf("teardown for suite\n");
}

void tc_add(cut_case_t *tc) {
        int a = 1;
        int b = 2;
        cut_int_equal(tc, 3, add(a, b), __LINE__);
}

void tc_sub(cut_case_t *tc) {
        int a = 3;
        int b = 1;
        cut_int_equal(tc, 1, sub(a, b), __LINE__); // 会导致断言错误
}

int main() {
        cut_suite_t *suite = NULL;

        CUT_TEST_BEGIN("test with cut");

        CUT_TEST_SUITE_INIT(suite);

        CUT_TC_ADD(suite, "test tpl_addition:", tc_add);
        CUT_TC_ADD(suite, "test tpl_subtraction:", tc_sub);
        CUT_TS_ADD(suite, my_setup, my_teardown);

        CUT_TEST_RUN();

        CUT_TEST_REPORT();

        CUT_TEST_END();

        return 0;
}

测试结果:
total tc is 2, and failed tc is 1

6、小结
这里仅仅是提出一种实现C Unit Testing Framework的方案,而且仅仅是证明其可行,其离成熟的程度还远得很。我们可以从已经成熟的单元测试工具那里借鉴很多东西过来,如Test Group概念、XML配置等。改进是永无止境的,任重道远啊:)。

CppUnit入门实践-Tony与Alex的对话系列

Tony : Hi Alex ! you just looks like drowing in your project. what is up?
Alex : 我们的项目要求引入单元测试,but i've no experience in unit test.
Tony : i think cppunit is your best choice.
Alex : 是的,我刚从网上把它down了下来,正准备研究它呢。
Tony : Really ? I have done some practice on unit test before. would you like me to join you?
Alex : Oh Tony, I'm so glad that you could help me !
Tony : My pleasue !
Alex : 我们从哪里开始呢?
Tony : The simplest case! 我们拿一个最简单的例子吧。now we have a class with the name "SimpleCalculator" and it has four basic methods 'add', 'sub', 'mul' and 'div', All we should do is to test whether these methods run as same as we expect. First of all , complete the "SimpleCalculator" class, Alex.
Alex : It is simple!

//SimpleCalculator.h
class SimpleCalculator{
 public :
  int add(int a, int b);
  int sub(int a, int b);
  int mul(int a, int b);
  int div(int a, int b);
};

//SimpleCalculator.cpp
int SimpleCalculator::add(int a, int b){
 return a+b;
}

int SimpleCalculator::sub(int a, int b){
 return a-b;
}

int SimpleCalculator::mul(int a, int b){
 return a*b;
}

int SimpleCalculator::div(int a, int b){
 return a/b;
}

Alex : 这里简单点,div方法没有考虑0作除数的异常情况。
Tony : 可以。还记得我上次讲的测试驱动开发么,不过今天我们不是用它,我们只做些简单的东西,目的就是为了熟悉工具的使用。
Alex : 那我们是不是也应该列出一个test case的list亚?
Tony : 没错。
Alex : 我来随意写几个吧。“add(5,6) == 11” 、“sub(5,6)==-1”、“mul(5,6) == 30”和“div(12,6) == 2”。
Tony : 然后我们一起来学习一下CppUnit的帮助文档吧。

(Tony and Alex are reading the doc of cppunit.)

Tony : 学到了些什么?
Alex : 看来这些xUnit框架的测试工具在概念上几乎是一致的,像TestCase、TestFixture和TestSuite这些概念都大同小异。
Tony : 不错,单元测试在于测试思想,工具只是个必要条件而已,工具并不能决定你的测试就是一个好的测试。下面你就按你理解的CppUnit去做吧。

Alex : Ok. 按照书中所说,我们一次要测试多个method,最好使用TestFixture。我是这样写的,你看看。

#include "SimpleCalculator.h"
#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"

class SimpleCalcTest : public CPPUNIT_NS::TestFixture{
private :
 SimpleCalculator * sc;

public:
 virtual void setUp(){
         sc = new SimpleCalculator();
     }
     virtual void tearDown(){
         delete sc;  
     }
 
 void testAdd(){       
         CPPUNIT_ASSERT_EQUAL( sc->add(5,6), 11);
     }

 void testSub(){       
         CPPUNIT_ASSERT_EQUAL( sc->sub(5,6), -1 );
     }

     void testMul(){       
         CPPUNIT_ASSERT_EQUAL( sc->mul(5,6), 30 );
     }

 void testDiv(){       
         CPPUNIT_ASSERT_EQUAL( sc->div(12,6), 2 );
     }
};

我们的主函数如下:
int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );

    CPPUNIT_NS::TestCaller testCase1( "testAdd", &SimpleCalcTest::testAdd );
    CPPUNIT_NS::TestCaller testCase2( "testSub", &SimpleCalcTest::testSub );
    CPPUNIT_NS::TestCaller testCase3( "testMul", &SimpleCalcTest::testMul );
    CPPUNIT_NS::TestCaller testCase4( "testDiv", &SimpleCalcTest::testDiv );
   
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
   
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

Tony : 我觉得可行。运行一下,看看如何。
Alex : 输出结果如下:
OK (4 tests)

Tony : 这的确是一种可行的办法,不过你回想一下我们一起学习的doc中的内容,看看是否还有改进的余地了。现在如果你要在SimpleCalcTest类中加一个测试用例方法,不仅仅SimpleCalcTest要修改,我们的main函数也需要修改,还记得JUnit中有什么概念来支持么?

Alex : 你不提我还真的记不起来了,JUnit中有TestSuite,刚才在cppunit doc中我也看到了suite方法,也许会帮得上忙,稍等一下我再翻翻文档….

Alex : 我找到了。的确有更为简单的方法。我修改一下,引用的头文件不变。

class SimpleCalcTest : public CPPUNIT_NS::TestFixture{

    CPPUNIT_TEST_SUITE( SimpleCalcTest );
        CPPUNIT_TEST( testAdd );
        CPPUNIT_TEST( testSub );
        CPPUNIT_TEST( testMul);
        CPPUNIT_TEST( testDiv );       
    CPPUNIT_TEST_SUITE_END();

private :
 SimpleCalculator * sc;

public:
 virtual void setUp(){
         sc = new SimpleCalculator();
     }
     virtual void tearDown(){
         delete sc;  
     }
 
 void testAdd(){       
         CPPUNIT_ASSERT_EQUAL( sc->add(5,6), 11);
     }

 void testSub(){       
         CPPUNIT_ASSERT_EQUAL( sc->sub(5,6), -1 );
     }

     void testMul(){       
         CPPUNIT_ASSERT_EQUAL( sc->mul(5,6), 30 );
     }

 void testDiv(){       
         CPPUNIT_ASSERT_EQUAL( sc->div(12,6), 2 );
     }
};

CPPUNIT_TEST_SUITE_REGISTRATION( SimpleCalcTest );

主函数修改后如下:
int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );

    CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()->run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

CppUnit利用宏来解决Suite的问题。在你的TestCase定义里面写入如下的这段代码:
CPPUNIT_TEST_SUITE( YourTestCase );
        CPPUNIT_TEST( testXX);
   …//
CPPUNIT_TEST_SUITE_END();
这段代码实际上是定义了一个函数suite,这个函数返回了一个包含了所有CPPUNIT_TEST定义的测试用例的一个测试集。CPPUNIT_TEST_SUITE_REGISTRATION通过静态注册把这个测试集注册到全局的测试树中,最后通过CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()生成一个包含所有测试用例的测试并且运行。这样的话,一旦要添加新的测试用例函数,我们只需要修改SimpleCalcTest类即可。

Tony : Well done! Alex你独立解决问题的能力越来越强了。相信做到这你已经心里有底儿了,再往后就是在你的实际项目中摸索CppUnit的使用经验了。

Alex : 呵呵。谢谢夸奖!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats