技术开发 频道

用cpp做C单元测试

【IT168 技术文章】

    介绍:

    在QA中,主要有两种测试

    单元测试:验证我们系统中的所有逻辑单元的验证行为(并不考虑其他单元的相互关系,比如其他的可以打成桩函数等。)

    系统测试(集成测试)各个单元之间的相互关系,检测系统运行行为。

    单元测试用例设计

    在开发过程中,程序员通常用调试器来测试他们的程序,但是很少有人去单步调试程序,不会检测每个可能的变量值,这样我们就要借助一些工具来完成。就是我们所说的“单元测试框架”来测试我们的程序。

    我们来测试一个简单的c程序          BOOL addition(int a, int b)
        {
            return (a + b);
        }


    我们的用例必须借助其他的c函数来完成验证所有的可能性,返回True或者False来说明测试是否通过          BOOL additionTest()
        {
            if ( addition(1, 2) != 3 )
                return (FALSE);
            if ( addition(0, 0) != 0 )
                return (FALSE);
            if ( addition(10, 0) != 10 )
                return (FALSE);
            if ( addition(-8, 0) != -8 )
                return (FALSE);
            if ( addition(5, -5) != 0 )
                return (FALSE);
            if ( addition(-5, 2) != -3 )
                return (FALSE);
            if ( addition(-4, -1) != -5 )
                return (FALSE);
            return (TRUE);
        }


    我们看到,测试所有的可能性需要

    正数+负数, 0+0, 负数+0, 正数+0,正数+正数,负数+正数,负数+负数

    每个cases比较了加的结果和期望值,如果不通过就False,如果都通过就返回True

    行为上可以设计下面的例子:          int additionPropertiesTest()
        {
            // conmutative: a + b = b + a
            if ( addition(1, 2) != addition(2, 1) )
                return (FALSE);
            // asociative: a + (b + c) = (a + b) + c
            if ( addition(1, addition(2, 3)) != addition(addition(1, 2), 3) )
                return (FALSE);
            // neutral element: a + NEUTRAL = a
            if ( addition(10, 0) != 10 )
                return (FALSE);
            // inverse element: a + INVERSE = NEUTRAL
            if ( addition(10, -10) != 0 )
                return (FALSE);
            return (TRUE);
        }


    但是这样当代码变化时用例就得跟着相应的变化,或者去加一个新的case

    XP(极限编程)推荐就是在编写代码之前先写测试用例。就是测试驱动开发。

        CPPUnit

        CPPUnit

    各Case应该被写在类里面从TestCase 导出。这个类对我们所有基本功能进行测试, 在Test Suite(测试用例集合)登记等等

    例如, 我们写了一个功能在磁盘存放一些数据的小模块。 这个模块(类名DiskData) 有主要二功能: 装载和保存数据到文件里面:          typedef struct _DATA
        {
            int number;
            char string[256];
        }
        DATA, *LPDATA;
        class DiskData
        {
            public:
                DiskData();
                ~DiskData();
            LPDATA getData();
            void setData(LPDATA value);
            bool load(char *filename);
            bool store(char *filename);
            private:
            DATA m_data;
        };


    现在, 什么编码方式并不重要, 因为最重要事是我们必须肯定它必须做, 是这个类应该做: 正确地装载和存放数据到文件。

    为了做这个验证,我们去创造一个新的测试集,包括二个测试用例: 一个装载数据和另为存储数据

    使用 CPPUnit
 
    你能在这里http://cppunit.sourceforge.net/得到最新的CPPUnit 版本, 你能发现所有的库 , 文献, 例子和其它有趣的材料。(我下载了版本为1.8.0 并且这个颁布工作良好)

    在Win32里, 你能在VC++ 之下(6.0 和以后版本)使用CPPUnit , 但是当CPPUnit 使用ANSI C++, 有少量接口时针对其它环境象C++Builder。

    在CPPUnit发布版本里面,所有建造库的步骤和信息,可以在INSTALL-WIN32.txt文件找到,。当所有二进制文件被构建之后, 你就能写你自己的测试集了。

    想在VC中写自己的测试程序,可以按照以下步骤:

    建立一个MFC的对话框(或文档视图结构)
    允许时间类型信息,Alt+F7 --> C/C++ --> C++ language --> Enable RTTI
    把Cppunit\inlude放到include目录:Tools - Options - Directories - Include.
    用cppunitd.lib (静态连接) 或者cppunitd_dll.lib (动态链接),testrunnerd.lib来链接你的程序。
    如果动态链接,就要把testrunnerd.dll 拷到应用程序目录来运行。
    Ok,看一下测试用例的类的定义吧。          #if !defined(DISKDATA_TESTCASE_H_INCLUDED)

        #define DISKDATA_TESTCASE_H_INCLUDED
        #if _MSC_VER > 1000
        #pragma once
        #endif // _MSC_VER > 1000
        #include <cppunit/TestCase.h>
        #include <cppunit/extensions/HelperMacros.h>
        #include "DiskData.h"
        class DiskDataTestCase : public CppUnit::TestCase
        {
            CPPUNIT_TEST_SUITE(DiskDataTestCase);
            CPPUNIT_TEST(loadTest);
            CPPUNIT_TEST(storeTest);
            CPPUNIT_TEST_SUITE_END();
            public:
            void setUp();
            void tearDown();
            protected:
            void loadTest();
            void storeTest();
            private:
            DiskData *fixture;
        };
        #endif


    首先, 必须包含TestCase.h和HelperMacros.h. 第一步,我们的从我们的Testcase基类配生的新类。第二,用一些宏使我们的定义的更方便,如 CPPUNIT_TEST_SUITE (开始测试定义), CPPUNIT_TEST (定义一个测试用例) 或 CPPUNIT_TEST_SUITE_END (结束一个测试集).

    我们的类(DiskDataTestCase)有重载了两个方法setUp()和tearDown(). 一个开始,一个结束测试。

    测试过程如下

    启动程序

    点击“Run”

    调用Call setUp()方法: 构建我们的测试对象fixture

    调用第一个测试方法

    调用tearDown() 方法,清除对象

    调用Call setUp()方法: 构建我们的测试对象fixture

    调用第一个测试方法

    调用Call setUp()方法: 构建我们的测试对象fixture

    就像下面的形式:          #include "DiskDataTestCase.h"
        CPPUNIT_TEST_SUITE_REGISTRATION(DiskDataTestCase);
        void DiskDataTestCase::setUp()
        {
            fixture = new DiskData();
        }
        void DiskDataTestCase::tearDown()
        {
            delete fixture;
            fixture = NULL;
        }
        void DiskDataTestCase::loadTest()
        {
            // our load test logic
        }
        void DiskDataTestCase::storeTest()
        {
            // our store test logic
        }

    编写测试用例

    一旦我们知道我们要测什么之后,我们就可以写测试用例了。我们能够执行所有的我们需要的操作:使用普通库函数,第三方库,win32api库函数,或简单使用c++内部操作

    有时候,我们需要调用外部辅助文件或者数据库,比较外部文件和内部数据是否一致。

    检测一个条件就使用
        CPPUNIT_ASSERT(condition):如果为false就抛出异常

        CPPUNIT_ASSERT_MESSAGE(message, condition): 如果为false就抛出制定的信息。

        CPPUNIT_ASSERT_EQUAL(expected,current): 检测期望值

        CPPUNIT_ASSERT_EQUAL_MESSAGE(message,expected,current): 当比较值不相等时候抛出的制定的信息。

        CPPUNIT_ASSERT_DOUBLES_EQUAL(expected,current,delta): 带精度的比较

        下面是测试loadTest的例子,          //
        // These are correct values stored in auxiliar file
        //
        #define AUX_FILENAME "ok_data.dat"
        #define FILE_NUMBER 19
        #define FILE_STRING "this is correct text stored in auxiliar file"
        void DiskDataTestCase::loadTest()
        {
            // convert from relative to absolute path
            TCHAR absoluteFilename[MAX_PATH];
            DWORD size = MAX_PATH;
            strcpy(absoluteFilename, AUX_FILENAME);
            CPPUNIT_ASSERT( RelativeToAbsolutePath(absoluteFilename, &size) );
            // executes action
            CPPUNIT_ASSERT( fixture->load(absoluteFilename) );
            // ...and check results with assertions
            LPDATA loadedData = fixture->getData();
            CPPUNIT_ASSERT(loadedData != NULL);
            CPPUNIT_ASSERT_EQUAL(FILE_NUMBER, loadedData->number);
            CPPUNIT_ASSERT( 0 == strcmp(FILE_STRING,
            fixture->getData()->string) );
        }


    在这个case我们得到四个可能的错误:          load method's return value
        getData method's return value
        number structure member's value
        string structure member's value

    第二个用例也是相似的。但是困难点,我们需要使用已知的数据来填充fixture,把它存在磁盘临时文件里,然后打开两个文件(新的和辅助文件),读并比较内容,两者如一致就正确          void DiskDataTestCase::storeTest()
        {
            DATA d;
            DWORD tmpSize, auxSize;
            BYTE *tmpBuff, *auxBuff;
            TCHAR absoluteFilename[MAX_PATH];
            DWORD size = MAX_PATH;
            // configures structure with known data
            d.number = FILE_NUMBER;
            strcpy(d.string, FILE_STRING);
            // convert from relative to absolute path
            strcpy(absoluteFilename, AUX_FILENAME);
            CPPUNIT_ASSERT( RelativeToAbsolutePath(absoluteFilename, &size) );
            // executes action
            fixture->setData(&d);
            CPPUNIT_ASSERT( fixture->store("data.tmp") );
            // Read both files contents and check results
            // ReadAllFileInMemory is an auxiliar function which allocates a buffer
            // and save all file content inside it. Caller should release the buffer.
            tmpSize = ReadAllFileInMemory("data.tmp", tmpBuff);
            auxSize = ReadAllFileInMemory(absoluteFilename, auxBuff);
            // files must exist
            CPPUNIT_ASSERT_MESSAGE("New file doesn't exists?", tmpSize > 0);
            CPPUNIT_ASSERT_MESSAGE("Aux file doesn't exists?", auxSize > 0);
            // sizes must be valid
            CPPUNIT_ASSERT(tmpSize != 0xFFFFFFFF);
            CPPUNIT_ASSERT(auxSize != 0xFFFFFFFF);
            // buffers must be valid
            CPPUNIT_ASSERT(tmpBuff != NULL);
            CPPUNIT_ASSERT(auxBuff != NULL);
            // both file's sizes must be the same as DATA's size
            CPPUNIT_ASSERT_EQUAL((DWORD) sizeof(DATA), tmpSize);
            CPPUNIT_ASSERT_EQUAL(auxSize, tmpSize);
            // both files content must be the same
            CPPUNIT_ASSERT( 0 == memcmp(tmpBuff, auxBuff, sizeof(DATA)) );
            delete [] tmpBuff;
            delete [] auxBuff;
            ::DeleteFile("data.tmp");
        }


    调用用户接口
    最后,我们看看用一个mfc 对话框(TestRunner.dll)用来说明。

    我们需要在我们的初始化函数中做如下初始化          #include <cppunit/ui/mfc/TestRunner.h>
        #include <cppunit/extensions/TestFactoryRegistry.h>
        BOOL CMy_TestsApp::InitInstance()
        {
            ....
            // declare a test runner, fill it with our registered tests and run them
            CppUnit::MfcUi::TestRunner runner;
            runner.addTest( CppUnit::TestFactoryRegistry::getRegistry().makeTest() );
            runner.run();
            return TRUE;
        }


    只要定义一个test的实例,然后注册所有用例,在跑case。
    每发现一个错误时9比如发现内部数据和外部数据不同我们就创建一个异常,使用 CPPUNIT_FAIL(message) 来显示异常信息。

0
相关文章