【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) 来显示异常信息。