UML软件工程组织

 

 

用cpp做c++单元测试
 
作者: SWVerification 来源: 软件测试验证小屋
 

介绍:

 在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中写自己的测试程序,可以按照以下步骤:

  1. 建立一个MFC的对话框(或文档视图结构)
  2. 允许时间类型信息,Alt+F7 --> C/C++ --> C++ language --> Enable RTTI
  3. 把Cppunit\inlude放到include目录:Tools - Options - Directories - Include.
  4. 用cppunitd.lib (静态连接) 或者cppunitd_dll.lib (动态链接),testrunnerd.lib来链接你的程序。
  5. 如果动态链接,就要把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++内部操作

有时候,我们需要调用外部辅助文件或者数据库,比较外部文件和内部数据是否一致。
每发现一个错误时9比如发现内部数据和外部数据不同我们就创建一个异常,使用 CPPUNIT_FAIL(message) 来显示异常信息。

检测一个条件就使用
 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。

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号