最近由于工作安排研究单元测试及CppUnit的使用,以下是我的研究心得:
一、 单元测试的研究
什么是单元测试
单元测试的定义是测试应用中最小的单元,单元测试被公认为软件开发过程中的一个关键步骤。单元测试能够简化错误检测,在减少开发时间和成本的同时提高软件质量。
为什么需要单元测试?
• 单元测试能帮助客户更准更全面地找到错误,显著提高软件质量
在集成测试中为了发现错误,我们希望通过不断修改输入,引发对象间的相互作用使得某对象引发潜在的错误,但这无疑是有难度的。由于其难度,开发人员只能依赖应用软件的运行失败来发现错误,这样不仅很难找到错误发生的准确位置,而且实际上还有大量的类没有得到测试。
单元级测试提供了一种更有效的发现错误的方法,它将应用程序中的最小单元分离开,使得测试更接近错误,只要简单地对每个最小单元进行独立测试,就很容易地使全面准确地找到所有的程序错误成为可能。
• 单元测试能够在改善软件质量的同时大量削减开发时间和成本
由于在较高的层次上修改一个类可能会改变多个程序部件的设计和功能性,因此越迟发现问题,通常就要修改越多的代码。当修改的代码量增加时,其他两个因素也会随之增加:
• 修改每一个错误所需的时间和费用
• 在代码中引入新的错误的机会
一次又一次的研究证明,随着问题被检测出来的时间的推迟,发现软件错误所需的时间和成本会惊人地增加。而单元测试由于能够更容易地找到错误,就会减少发现它们的时间和资源。
其次,由于你每完一个类,就能发现和改正其中的错误,你就不需要在以后花费大量时间重新了解和摸索。
最后,最重要的理由是由于类的相互作用和关联性,在单元级修改一个类只会影响到原始的类,避免了各个单元间的相互作用引发新的错误。因此单元测试能保证大大削减开发的时间和成本。
什么是单元测试的难点 ?
基于上述信息,单元测试看上去就象一剂万能药如果是这样的话,为什么每一个 C/C++ 开发人员不马上对每一个类进行单元测试?就目前可以使用的技术来说,对 C/C++ 的单元测试是一件困难、烦琐和耗时的事情,没有很好的工具来自动化这一过程,使得许多 C/C++ 开发人员望而生畏。
执行单元测试的第一步是是目标类变得可测。这需要两个工作:
• 设计一个运行目标类的测试驱动程序
• 设计桩函数,它们为被测类所引用的任何外部资源返回值
建立一个测试驱动,需要建立一个新的类,除了测试原始类以外它不能用于任何其它目的。测试驱动应该具有下列特性:
• 一个指定设置和清除的标准方式
• 一个选择个别测试和所有有效测试的方法
• 一个分析输出的预期(或非预期)结果的机制
• 一个标准的错误报告形式
为了充分而正确地测试类,你需要设计一个能够完全检查被测类的测试驱动;若干次修改和重写这样一个测试驱动是免不了的。一旦建立了测试驱动,你必须仔细检查它不能包含任何错误。测试驱动中的一个错误会破坏这个测试,但是你无法单独测试一个类,你也不能测试测试驱动本身。
如果你的类引用任何还没有准备好或不可访问的外部资源(如外部文件、数据库和 CORBA 对象等),你必须建立相应的桩函数,它们的返回值类似于这些实际的外部资源应该返回的。当建立这些桩函数时,你需要选择桩函数的返回值,它们将影响程序的执行路径:
• 为了测试类的功能性必须执行任何的路径
• 足够的路径能够提供彻底的测试覆盖性
下一步是设计和建立合适的测试用例。为了彻底地测试类的结构和功能性,你应该设计两种类型的
测试用例:黑盒和白盒。
黑盒测试
黑盒测试用例基于说明和规格文档。特别地,至少应该为规格文档的每个入口建立一个测试用例,更好的是这些测试用例能够测试每个入口的各种边界条件,还需要为发现的每一个错误增加另外的测试用例以及任何你认为必要的其它测试。
白盒测试
为了建立有效的白盒测试用例,你必须研究类的内部结构,然后编写测试用例尽可能完全地覆盖类的所有方法,以及覆盖所有可能引起类崩溃的输入。要达到较高的测试覆盖性,需要有效的白盒测试用例,并且要求它们能够执行相当多的路径。例如,一个典型的万行的程序,大约有上亿条可能的路径,而手工建立能够执行所有这些路径的输入几乎是不可能的。
回归测试
任何时候一个类被修改后,你应该执行回归测试,保证没有引入新的错误和 / 或原来的错误已经被更正了。回归测试包括白盒和黑盒测试用例,并且分析结果以确定类的质量是否得到了改善
覆盖率测试
在建立测试用例以后,你将要执行整个测试用例并分析结果,确定在那里出现了错误崩溃和薄弱环节你还需测量这些测试的覆盖性,以确定类被测试的程度以及需要追加的测试用例。
二、 CppUnit的初步了解
CppUnit是xUnit系列中的开源c++实现版本,它是从JUnit移植过来的,第一个移植版本由Michael Feathers完成,相关信息可以在http://www.xprogramming.com/software.htm找到。可以从http://sourceforge.net/projects/cppunit/下载到最新版本。
为什么要用CppUnit?
cppunit规范了单元测试的一些思想且很好的引入了测试驱动开发的概念。这些思想朴素但非常重要。cppunit的测试观念主要有:
- 测试的结果是程序直接监测的,而不是“通过人眼对屏幕上的输出结果的观测”。
因此,cppunit并不推荐屏幕输出,或者写可视化的测试单元。
- 测试的过程是自动化的,不需要人工的干预。
cppunit推荐用大量典型测试数据进行回归的方式。
- 测试案例是安全可控的。
如果一个测试案例错误或者发生了异常,那么应该记录这个错误,并且去执行下一个案例,而不应该停下来。cppunit测试框架保证了这一点。
- 单元测试是频繁发生的,每天都进行。
由于测试案例的自动化,故此,在你的模块发生了重要改变时(特别是设计上的重大变化/重构时),你都应该马上运行一遍所有的测试程序,以确认你的代码没有引入预期(或曾经出现过)的bug。你可以在准备吃饭的时候,启动单元测试程序进行回归。
- 单元测试的目的是产生高质量的单元(模块)。从而减少系统集成(包括系统集成测试)的代价。
CppUnit的总体构成
作为一个完整的CppUnit framework,虽然源码所在的实际路径可能不尽相关,但从逻辑上讲它们被划为如下几个部分:
- core:CppUnit的核心部分
- output:掌管结果输出
- helper:一些辅助类
- extension:作为单元测试的延伸,对CppUnit core部分的扩展(比如:常规测试,重复测试)
- listener:监视测试进程和测试结果
- textui:一个运行单元测试的文本环境
- portability:提供针对不同平台的移植设置
上述所有的内容均被置于CppUnit名字空间之内。
CppUnit与 TDD(测试驱动开发)
1)测试驱动开发精髓:
a)维护详尽的程序员编写的测试程序组
b)除非有相关的测试,否则代码不应被加入产品(“极限编程”,因为测试是重要的,所以对几乎所有代码都要有测试)
c)测试先行
d)测试决定你需要写的代码
2)测试驱动(TDD)开发的重要原则:
“Test twice, code once” - 测试两次,编码一次。
步骤:(TDD循环)
a)编写新代码的测试,查看是否失败
b)编写新代码,以最简方式实现
c)再次测试是否成功,重构代码
三、 在VS2005如何使用CppUnit
1. 编译cppunit
目前在vs2005下直接编译cppunit工程是编译不过去的。还要对cppunit工程做一下适当的改动才能成功编译。
虽然网上有些文章介绍使用CPPUnitProjectWizard。但是我查阅了相关资料:发现其实不用安装这个CPPUnitProjectWizard。也可以在vs2005下成功编译和链接Cppunit。
首先,在对CppUnitLibraries解决方案做编译之前,需要在硬盘的cppuint目录下新建一个与src文件夹同级的lib文件夹。在1.12.0版本下这个问题已经解决。但是在1.12之前的版本还需要手动去建lib文件夹。
其次:在编译TestRunner这个工程之前,需要先对MsDevCallerListCtrl.cpp文件做一下更改。将
#import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("7.0") lcid("0") raw_interfaces_only named_guids
改为
#import "libid:80cc9f66-e7d8-4ddd-85b6-d9e6cd0e93e2" version("8.0") lcid("0") raw_interfaces_only named_guids
这样将会从"%common_files%/MicrosoftShared/MSEnv/dte80a.olb"导入 dte80a.olb.最后,将DSPlugIn这个工程从解决方案里删掉,因为它只支持vc6.0。做过这些处理后vs2005就应该可以成功编译cppunit。
2. vs2005下的环境配置
开发环境中,在vs2005的工具菜单下选项->项目和解决方案- > vc++目录设置好Include 和Lib引用路径。例如:在包含文件选项下加入C:/cppunit/lib 。在库文件选项下加C:/cppunit/include 。另外最好在计算机的系统环境变量下将lib路径添加到path变量下,这样就不用把dll拷来拷去。
3. 一个基于GUI的界面测试程序的例子
首先建一个基于扩展mfc的动态库工程DemoDll。
再新建一个CDemo类(被测试类)
头文件
#pragma once
class AFX_EXT_CLASS CDemo
{
public:
CDemo(void);
virtual ~CDemo(void);
int Add(int i);
int Subtration(int i);
public:
int m_iOper;
};
cpp文件
#include "StdAfx.h"
#include "Demo.h"
CDemo::CDemo(void)
{
m_iOper = 0;
}
CDemo::~CDemo(void)
{
}
int CDemo::Add(int i)
{
return m_iOper += i;
}
int CDemo::Subtration(int i)
{
return m_iOper -= i;
}
在同个一解决方案下新建一个测试工程用于测试这个Demo类
新建一个基于Dialog的工程DemoMfcUnitTest。在项目属性里链接器->输入->附加依赖项下输入cppunitd_dll.lib 、testrunnerd.lib (DEBUG)或cppunit_dll.lib 、testrunner.lib (Release)并需要将被测试类的头文件路径添加到项目属性里c/c++->附加包含目录里。例如:../ DemoDll。同时在项目依赖项中把Demodll设为依赖项。另外,要注意保证这两个工程的输出目录相同。
在App类中添加下列头文件
#include "cppunit/extensions/TestFactoryRegistry.h"
#include "cppunit/ui/mfc/TestRunner.h"
#include "TDemoMfc_CDemo.h" // 测试用例类头文件
并在IninInstance()函数中把显示对话框的代码注销掉并在原地曾加下列代码用以显示测试界面。
CppUnit::MfcUi::TestRunner runner;
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(CTDemoMfc_CDemo::RegCaseName());
runner.addTest(registry.makeTest());
runner.run();
return true;
之后构建一个测试用例类。
头文件:
#pragma once
#include "cppunit/TestCase.h"
#include "CppUnit/extensions/HelperMacros.h"
#include <string.h>
#include "Demo.h" // 被测试类
class CTDemoMfc_CDemo : public CppUnit::TestCase
{
// 宏定义
CPPUNIT_TEST_SUITE(CTDemoMfc_CDemo); // 开始声明一个新的测试程序集
CPPUNIT_TEST(TestAdd); // 添加TestAdd测试函数到测试程序集
CPPUNIT_TEST(TestSubtration); // 添加TestSubtration测试函数到测试程序集
CPPUNIT_TEST_SUITE_END(); // 声明结束
public:
CTDemoMfc_CDemo(void);
void TestAdd();
void TestSubtration();
static std::string RegCaseName() { return "CTDemoMfc_CDemo";}
public:
~CTDemoMfc_CDemo(void);
};
Cpp文件
#include "StdAfx.h"
#include "TDemoMfc_CDemo.h"
#include "cppunit/extensions/TestFactoryRegistry.h"
#include "cppunit/TestAssert.h"
// 对指定程序集进行注册
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CTDemoMfc_CDemo,CTDemoMfc_CDemo::RegCaseName());
CTDemoMfc_CDemo::CTDemoMfc_CDemo(void)
{
}
CTDemoMfc_CDemo::~CTDemoMfc_CDemo(void)
{
}
void CTDemoMfc_CDemo::TestAdd()
{
CDemo cDemo;
int iResult = cDemo.Add(10);
// 宏判断两个值是否相等
CPPUNIT_ASSERT_EQUAL(iResult,10);
}
void CTDemoMfc_CDemo::TestSubtration()
{
CDemo cDemo;
int iResult = cDemo.Subtration(10);
// 宏判断两个值是否相等
CPPUNIT_ASSERT_EQUAL(iResult,-10);
}
最后编译并运行这个工程就会看到CppUnit的GUI测试界面。
关于Cppunit详细的介绍,可以访问http://morningspace.51.net/resource/cppunit/cppunit_anno.html。这上面对cppunit的描述即全面又详细。