1 单元测试对象概述
各个对象组织结构如下:
职责简述如下:
对象 |
职责 |
TestAssert |
测试断言:判定测试结果是否正确,一般类似断言表达。 |
TestCase |
测试用例:多个测试断言组成一个测试用例。测试对象为一个类中的一个具体方法。 |
TestSuite |
测试套件:多个测试用例组成一个测试套件。测试对象为一个类。 |
MainTestSuite |
主测试套件:单元测试运行主程序入口。测试用例也可绕过测试套件,直接包含在主测试套件中。 |
TestFixture |
测试夹具:用于测试前的初始化操作以及测试后的清理操作,一般包括准备测试的前置条件/测试对象的状态设置等。 |
2 单元测试框架选型
当前存在很多流行的单元测试框架:衍生自JUnit的CppUnit,以及简化版本的CppUnitLite,Boost.Test测试框架,Google Test测试框架等。每个测试框架都很完善,都可胜任单元测试任务。
从使用简单性考虑,依次是Boost.Test>>Google Test>>CppUnitLite>>CppUnit.
本文选择Boost.Test的单元测试框架讲解。对Google Test感兴趣的可参看http://www.cnblogs.com/coderzh/archive/2009/03/31/1426758.html。
3 Boost.Test UTF(Unit Test Framework)
3.1 LogLevel
讲解TestAssert前,先说下Boost测试框架的日志级别,有以下9个级别:
级别名称 |
说明 |
all / success |
报告包括成功测试通知的所有日志信息 |
test_suite |
显示测试套件信息 |
message |
显示用户信息 |
warning |
报告用户发出的警告 |
error |
报告所有错误情况 |
cpp_exception |
报告未捕获的 C++ 异常 |
system_error |
报告系统引起的非致命错误 (例如,超时或浮点数异常) |
fatal |
用户或系统引起的致命错误 (例如,内存访问越界) |
nothing |
不报告任何信息 |
生成测试可执行程序后,可以通过指定--log_level参数指定日志接别。比如,最后可执行程序为testmini,执行./testmini --log_level=warning指定在warning级别运行,默认执行在error级别。
请特别关注级别中黑体部分,TestAssert中将使用到。
3.2 TestAssert
Boost.Test中测试断言包含如下3大类:
类别 |
功能 |
说明 |
WARN |
打印warning日志,不增加失败引用计数,继续执行程序 |
检验不太重要但是正确的方面 |
CHECK |
打印error日志,增加失败引用计数,继续执行程序 |
实现 assertions |
REQUIRE |
增加失败应用计数,中断程序的运行 |
失败就不应该让程序继续运行则使用 |
Boost.Test中详细测试断言包含如下几种:
TestAssert类型 |
说明 |
举例 |
BOOST_WARN |
WARN型预言检测 |
BOOST_WARN(2+2==4); |
BOOST_CHECK |
CHECK型预言检测 |
BOOST_CHECK(2+2==4); |
BOOST_REQUIRE |
REQUIRE型预言检测 |
BOOST_REQUIRE(2+2==4); |
BOOST_WARN_MESSAGE |
WARN型预言检测,自定义日志 |
BOOST_WARN_MESSAGE(2+2==4,"description…" ); |
BOOST_CHECK_MESSAGE |
CHECK型预言检测,自定义日志 |
BOOST_CHECK_MESSAGE(2+2==4,"description…" ); |
BOOST_REQUIRE_MESSAGE |
REQUIRE型预言检测,自定义日志 |
BOOST_ REQUIRE _MESSAGE(2+2==4,"description…" ); |
BOOST_ERROR |
同BOOST_CHECK_MESSAGE( false, M ) |
if( 2+2 !=4 ) BOOST_ERROR( "description…" ); |
BOOST_FAIL |
同BOOST_REQUIRE_MESSAGE( false, M ) |
if( 2+2 !=4 ) BOOST_FAIL( "description…" ); |
BOOST_WARN_EQUAL |
WARN型左右值相等检测检测 |
BOOST_WARN_EQUAL(2+2,4); |
BOOST_CHECK_EQUAL |
CHECK型左右值相等检测检测 |
BOOST_CHECK_EQUAL(2+2,4); |
BOOST_REQUIRE_EQUAL |
REQUIRE型左右值相等检测检测 |
BOOST_REQUIRE_EQUAL(2+2,4); |
该类还有不等,小于,大于,小于等于,大于等于的判别检测,此处只罗列WARN的,其他不再一一罗列:BOOST_WARN_NE/ BOOST_WARN_LT/ BOOST_WARN_LE/ BOOST_WARN_GT/ BOOST_WARN_GE |
||
BOOST_WARN_THROW |
WARN型,判别执行函数期,有异常抛出。 |
BOOST_WARN_THROW(executeSql(“select…”),oracle::ErrorCodeException); |
BOOST_CHECK_THROW |
CHECK型,判别执行函数期,有异常抛出。 |
|
BOOST_REQUIRE_THROW |
REQUIRE型,判别执行函数期,有异常抛出。 |
|
BOOST_WARN_EXCEPTION |
WARN型,执行函数,当预言为真时,捕获异常。 |
BOOST_WARN_THROW(executeSql(“select…”),oracle::ErrorCodeException,2+2==4);
|
BOOST_CHECK_EXCEPTION |
CHECK型,执行函数,当预言为真时,捕获异常。 |
|
BOOST_REQUIRE_EXCEPTION |
REQUIRE型,执行函数,当预言为真时,捕获异常。 |
|
BOOST_WARN_NO_THROW |
WARN型,判别执行函数期,无异常抛出。 |
BOOST_WARN_NO_THROW(executeSql(“select…”)); |
BOOST_CHECK_NO_THROW |
CHECK型,判别执行函数期,无异常抛出。 |
|
BOOST_REQUIRE_NO_THROW |
REQUIRE型,判别执行函数期,无异常抛出。 |
|
BOOST_WARN_CLOSE |
WARN型,判定左右值是否足够逼近。用于浮点数比较。 |
BOOST_WARN_CLOSE(2.1131,2.1132,0.01) |
BOOST_CHECK_CLOSE |
CHECK型,判定左右值是否足够逼近。用于浮点数比较 |
|
BOOST_REQUIRE_CLOSE |
REQUIRE型,判定左右值是否足够逼近。用于浮点数比较 |
|
BOOST_WARN_SMALL |
WARN型,判定值是否足够小(是否接近0)。用于浮点数比较 |
BOOST_WARN_CLOSE(0.1,0.01) |
BOOST_CHECK_SMALL |
CHECK型,判定值是否足够小(是否接近0)。用于浮点数比较 |
|
BOOST_REQUIRE_SMALL |
REQUIRE型,判定值是否足够小(是否接近0)。用于浮点数比较 |
|
3.3 TestCase
对于TestCase/TestSuite等,Boost.Test既支持手动注册方式,也支持自动注册方式,当前Boost官方推荐自动注册方式,手动注册为了保持向前兼容保留,以后版本可能被移除。使用宏BOOST_AUTO_TEST_CASE即可自动注册测试用例。使用如下:
#include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_CASE(test_case_name) { BOOST_CHECK(true); } |
3.4 TestSuite
使用宏BOOST_AUTO_TEST_SUITE(test_suite_name)开始测试套件,使用BOOST_AUTO_TEST_SUITE_END()结束测试套件。使用举例:
#include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_SUITE(test_suite_name) BOOST_AUTO_TEST_CASE(test_case1) { BOOST_CHECK(true); } BOOST_AUTO_TEST_CASE(test_case1) { BOOST_CHECK(true); } BOOST_AUTO_TEST_SUITE_END() |
3.5 MainTestSuite
使用宏BOOST_TEST_MODULE表明主测试套件,一个测试项目中只能存在一个主测试套件。使用举例:
#define BOOST_TEST_MODULE maintest #include <boost/test/unit_test.hpp> |
3.6 TestFixture
测试夹具做测试前的准备工作和测试后的清理工作。而C++的RAII机制(构造函数申请资源,析构函数释放资源)恰好能满足该需求。因此Boost中直接使用普通类做夹具。实现夹具举例:该夹具在测试前将整数i初始化为5
struct MyFixture { MyFixture():i(5){} ~MyFixture(){} Int I; }; |
夹具可以和TestCase一起使用,也可以和TestSuite一起使用,也可以和MainTestSuite一起使用。
使用宏BOOST_FIXTURE_TEST_CASE(test_case_name, fixure_name)代替BOOST_AUTO_TEST_CASE(test_case_name)即可在TestCase中使用夹具,举例如下:
BOOST_FIXTURE_TEST_CASE(test_case_name,MyFixture) { BOOST_CHECK(i==5); } |
使用宏BOOST_FIXTURE_TEST_SUITE(suite_name, fixure_name)代替BOOST_AUTO_TEST_SUITE(suite_name)即可在TestSuite中使用夹具,举例如下:
BOOST_FIXTURE_TEST_SUITE(test_suite, MyFixture ); BOOST_AUTO_TEST_CASE (test_case1) { BOOST_CHECK(i==5); } BOOST_AUTO_TEST_CASE(test_case2) { BOOST_CHECK(++i==6); } BOOST_AUTO_TEST_SUITE_END() |
使用宏BOOST_GLOBAL_FIXTURE(MyFixture);将MyFixture声明为全局夹具,即可和MainTestSuite一起使用。
3.7 Boost.Test使用
对已经完成的项目做单元测试,假定该项目具有很好的测试性:
1) 对项目中的每个类对象创建一个测试套件,一个测试套件对应一个cpp文件。对类的每个类方法创建一个测试用例,这些测试用例均包含上前面的测试套件中。每个测试用例可以有多个测试断言,对该方法进行充分测试。
2) 在测试主文件中定义宏BOOST_TEST_MODULE,并包含所有的测试套件文件。
3) Linux下,将被测试项目编译成静态库(将main函数外的所有文件编译打包)供测试项目连接。Window下为测试项目做静态库工程,设置测试工程依赖该工程。并将头文件路径设置正确,即可编译运行。
附件为一示例项目以及对应单元测试工程举例,项目目录下make编译生成静态库以及可执行程序,test目录下make生成单元测试可执行程序。(略)
4 实行单元测试
4.1 现实困难
1、 内部依赖问题
类之间相互协作共同完成功能,类之间的依赖必不可少。为了测试某个类,必须实例化它依赖的类,而它依赖的类又可能依赖其他类,因此必须实例化其他类。如此一环扣一环,可能把整个项目大部分类都包含在了这次测试中,最后做的不是单元测试,而是挂着单元测试外壳的集成测试。
2、 外部依赖问题
很多项目,尤其是我们的网络应用服务器,运行期间需要依赖外部的其他服务器或者数据库或者本地的文件系统。而对很多外部的依赖很难模拟,或者说模拟成本太高,往往让测试者望而却步。
3、 函数本身问题
项目中的很多或者可能是大部分函数,是没有明确返回值或者无异常抛出,而只是和其他外设交互。难于使用测试断言判定。
以上造成很难将某个类从项目中隔离出来,难以设置单元测试点。
4.2 解决困难
上述困难均为依赖造成。
1、内部解依赖
对被测代码进行解依赖,强化设计,减少耦合,提高代码可测性。解依赖的过程也即为对代码重构过程,减少类间耦合,制造接口层。常用手段有:虚函数、函数指针、传递参数等方式。而对于难于进行解依赖情况,就要考虑提取分化重写方法。
2、写桩代码模拟外部环境
单元测试不能直接依赖外部环境,必须写桩代码模拟。而外部环境的可模拟性与内部解依赖紧密相关。对于外部的随机性和各种不确定性,桩代码必须尽可能模拟。
3、开辟访问类私有属性通道
有些类方法虽然没有明确返回值,但可能修改类的内部状态,可以通过判断类的私有属性来判定类方法的执行情况。可以给类增加Get()方法或者将私有属性设置为protected。
更重要的是在代码开发期,引入TDD思维,强化设计,提高代码可测性,提高代码的整体质量