本文是有关用于单元测试的开放源代码工具系列的第二篇文章,介绍了非常流行的CppUnit-JUnit测试框架的C++
端口,该端口最初由Eric Gamma和Kent Beck开发。 C++
端口是由Michael Feathers创建的,它包含各种类,可以帮助白盒测试和创建自己的回归套件。 本文介绍了一些更有用的CppUnit功能,例如TestCase,TestSuite,TestFixture,TestRunner和帮助程序宏。
为了本文的目的,我将CppUnit下载并安装在具有g ++-3.2.3和make-3.79.1的Linux®计算机(内核2.4.21)上。 安装简单且标准:运行configure
命令,然后执行make
和make install
。 请注意,对于某些平台(例如cygwin),此过程可能无法顺利运行,因此请务必查看安装随附的INSTALL-unix文档以获取详细信息。 如果安装成功,则应该在安装路径中看到CppUnit的include和lib文件夹,我们将其称为CPPUNIT_HOME。 清单1显示了文件结构。
[arpan@tintin] echo $CPPUNIT_HOME
/home/arpan/ibm/cppUnit
[arpan@tintin] ls $CPPUNIT_HOME
bin include lib man share
要编译使用CppUnit的测试,必须构建源:
g++ <C/C++ file> -I$CPPUNIT_HOME/include –L$CPPUNIT_HOME/lib -lcppunit
请注意,如果使用的是CppUnit共享库版本,则可能需要使用–ldl
选项来编译源。 安装后,您可能还需要修改UNIX®环境变量LD_LIBRARY_PATH以反映libcppunit.so的位置。
学习CppUnit的最佳方法是创建叶级测试。 CppUnit附带了很多预定义的类,您可以在设计测试时充分利用它们。 为了保持连续性,请回顾本系列第1部分中讨论的设计欠佳的字符串类(请参见清单2 )。
#ifndef _MYSTRING
#define _MYSTRING
class mystring {
char* buffer;
int length;
public:
void setbuffer(char* s) { buffer = s; length = strlen(s); }
char& operator[ ] (const int index) { return buffer[index]; }
int size( ) { return length; }
};
#endif
一些典型的与字符串相关的检查包括:验证空字符串的大小为0,并从字符串中访问索引超出范围会导致错误消息/异常。 清单3使用CppUnit进行这种测试。
#include <cppunit/TestCase.h>
#include <cppunit/ui/text/TextTestRunner.h>
class mystringTest : public CppUnit::TestCase {
public:
void runTest() {
mystring s;
CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() != 0);
}
};
int main ()
{
mystringTest test;
CppUnit::TextTestRunner runner;
runner.addTest(&test);
runner.run();
return 0;
}
您将学习的CppUnit代码库中的第一类是TestCase
。 要为字符串类创建单元测试,您需要将CppUnit::TestCase
类子类化,并覆盖runTest
方法。 现在已经定义了测试本身,请实例化TextTestRunner
类,该类是必须向其中添加单个测试的控制器类( vide addTest
方法)。 清单4显示了run
方法的输出。
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0
1) test: (F) line: 26 try.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero
为确保断言有效,请在宏CPPUNIT_ASSERT_MESSAGE
取反条件。 清单5显示了条件为s.size() ==0
时代码的输出。
[arpan@tintin] ./a.out
OK (1 tests)
请注意, TestRunner
不是运行单个测试或测试套件的唯一方法。 CppUnit提供了备用的类层次结构(即,模板化的TestCaller
类)来运行测试。 可以使用TestCaller
类代替runTest
方法来执行任何方法。 清单6提供了一个小示例。
class ComplexNumberTest ... {
public:
void ComplexNumberTest::testEquality( ) { … }
};
CppUnit::TestCaller<ComplexNumberTest> test( "testEquality",
&ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );
在上面的示例中,使用方法testEquality
(测试两个复数的相等性)定义了ComplexNumberText
类型的类。 TestCaller
该类对TestCaller
进行模板化,就像TestRunner
,您可以调用run
方法来执行测试。 但是,按原样使用TestCaller
类没有多大用处: TextTestRunner
类自动处理输出的显示。 对于TestCaller
,您必须使用单独的类来处理输出。 当您使用TestCaller
类定义自定义测试套件时,将在本文后面看到这种类型的代码流。
CppUnit为最常见的断言场景提供了几个例程。 这些被定义为CppUnit::Asserter
类的公共静态方法,可在标头Asserter.h中使用。 标头TestAssert.h中也为大多数这些类提供了预定义的宏。 在清单2中 ,这是CPPUNIT_ASSERT_MESSAGE
的定义方式(请参见清单7 ):
#define CPPUNIT_ASSERT_MESSAGE(message,condition) \
( CPPUNIT_NS::Asserter::failIf( !(condition), \
CPPUNIT_NS::Message( "assertion failed", \
"Expression: " \
#condition, \
message ), \
CPPUNIT_SOURCELINE() ) )
清单8提供了断言所基于的failIf
方法的声明。
struct Asserter
{
…
static void CPPUNIT_API failIf( bool shouldFail,
const Message &message,
const SourceLine &sourceLine = SourceLine() );
…
}
如果failIf
方法中的条件变为True,则将引发异常。 run
方法在内部处理此过程。 另一个有趣且有用的宏是CPPUNIT_ASSERT_DOUBLES_EQUAL
,它使用容差值(因此|expected – actual | ≤ delta
CPPUNIT_ASSERT_DOUBLES_EQUAL
)检查两个双精度数是否相等。 清单9提供了宏定义。
void CPPUNIT_API assertDoubleEquals( double expected,
double actual,
double delta,
SourceLine sourceLine,
const std::string &message );
#define CPPUNIT_ASSERT_DOUBLES_EQUAL(expected,actual,delta) \
( CPPUNIT_NS::assertDoubleEquals( (expected), \
(actual), \
(delta), \
CPPUNIT_SOURCELINE(), \
"" ) )
测试mystring
类的不同方面的一种方法是继续在runTest
方法内添加更多检查。 但是,除了最简单的类之外,快速地进行操作变得难以管理。 这是您需要定义和使用测试套件的地方。 请看清单10 ,它为您的字符串类定义了一个测试套件。
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
class mystringTest : public CppUnit::TestCase {
public:
void checkLength() {
mystring s;
CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
}
void checkValue() {
mystring s;
s.setbuffer("hello world!\n");
CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
}
CPPUNIT_TEST_SUITE( mystringTest );
CPPUNIT_TEST( checkLength );
CPPUNIT_TEST( checkValue );
CPPUNIT_TEST_SUITE_END();
};
那很简单。 您使用CPPUNIT_TEST_SUITE
宏定义测试套件。 属于mystringTest
类的各个方法构成了测试套件中的单元测试。 我们将稍后检查这些宏及其内容,但首先,请看一下清单11中使用此测试套件的客户端代码。
CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTest );
int main ()
{
CppUnit::Test *test =
CppUnit::TestFactoryRegistry::getRegistry().makeTest();
CppUnit::TextTestRunner runner;
runner.addTest(test);
runner.run();
return 0;
}
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run: 2 Failures: 2 Errors: 0
1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero
2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual : w
- Corrupt String Data
CPPUNIT_ASSERT_EQUAL_MESSAGE
在标头TestAssert.h中定义,并检查预期参数和实际参数是否匹配。 如果否定,则显示指定的消息。 在HelperMacros.h中定义的CPPUNIT_TEST_SUITE
宏简化了创建测试套件并向其中添加单个测试的过程。 在内部,创建类型为CppUnit::TestSuiteBuilderContext
的模板化对象(这等效于CppUnit上下文中的测试套件),并且每次调用CPPUNIT_TEST
向该套件中添加相应的类方法。 不用说,是对代码进行单元测试的类方法。 请注意宏的顺序:要编译单个CPPUNIT_TEST
宏的代码,它必须在CPPUNIT_TEST_SUITE
和CPPUNIT_TEST_SUITE_END
宏之间。
随着时间的推移,开发人员不断在代码中添加功能,这需要进一步的测试。 继续向同一测试套件中添加测试会随着时间的流逝而增加混乱,而最初为之开发测试的更改的增量性质却丢失了。 幸运的是,CppUnit有一个有用的宏,称为CPPUNIT_TEST_SUB_SUITE
,可用于扩展现有的测试套件。 清单13使用了此宏。
class mystringTestNew : public mystringTest {
public:
CPPUNIT_TEST_SUB_SUITE (mystringTestNew, mystringTest);
CPPUNIT_TEST( someMoreChecks );
CPPUNIT_TEST_SUITE_END();
void someMoreChecks() {
std::cout << "Some more checks...\n";
}
};
CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTestNew );
请注意,新类mystringTestNew
继承自先前的myStringTest
类。 CPPUNIT_TEST_SUB_SUITE
宏接受新类及其超类作为两个参数。 在客户端,您只需注册新类而不是两个类。 就是这样:其余语法与创建测试套件几乎相同。
夹具或CppUnit上下文中的TestFixture
旨在为各个测试提供干净的设置和退出例程。 要使用灯具,可以从CppUnit::TestFixture
派生测试类,并覆盖预定义的setUp
和tearDown
方法。 在执行单元测试之前,将调用setUp
方法,在执行测试时,将调用tearDown
。 清单14显示了如何使用TestFixture
。
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
class mystringTest : public CppUnit::TestFixture {
public:
void setUp() {
std::cout << “Do some initialization here…\n”;
}
void tearDown() {
std::cout << “Cleanup actions post test execution…\n”;
}
void checkLength() {
mystring s;
CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
}
void checkValue() {
mystring s;
s.setbuffer("hello world!\n");
CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
}
CPPUNIT_TEST_SUITE( mystringTest );
CPPUNIT_TEST( checkLength );
CPPUNIT_TEST( checkValue );
CPPUNIT_TEST_SUITE_END();
};
[arpan@tintin] ./a.out
. Do some initialization here…
FCleanup actions post test execution…
. Do some initialization here…
FCleanup actions post test execution…
!!!FAILURES!!!
Test Results:
Run: 2 Failures: 2 Errors: 0
1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero
2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual : w
- Corrupt String Data
从该输出中可以看到,设置和拆卸例程消息每执行一次单元测试一次。
无需使用任何帮助程序宏就可以创建测试套件。 使用一种样式相对于另一种样式没有特别的好处,但是非宏编码方式使调试更加容易。 要创建没有宏的测试套件,请实例化CppUnit::TestSuite
,然后将单个测试添加到套件中。 最后,在调用run
方法之前,将套件本身传递给CppUnit::TextTestRunner
。 客户端代码几乎相同,如清单16所示 。
int main ()
{
CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
&mystringTest::checkLength));
suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
&mystringTest::checkLength));
// client code follows next
CppUnit::TextTestRunner runner;
runner.addTest(suite);
runner.run();
return 0;
}
要理解这是怎么回事在上市16 ,你需要了解从CppUnit的命名空间的两个类: TestSuite
和TestCaller
,在TestSuite.h和TestCaller.h宣布,分别。 当执行runner.run()
调用时,CppUnit的内部将为每个单独的TestCaller
对象调用runTest
方法,该对象依次调用传递给TestCaller<mystringTest>
构造函数的例程。 清单17显示了代码(来自CppUnit来源),该代码说明了如何为每个套件调用单独的测试。
void
TestComposite::doRunChildTests( TestResult *controller )
{
int childCount = getChildTestCount();
for ( int index =0; index < childCount; ++index )
{
if ( controller->shouldStop() )
break;
getChildTestAt( index )->run( controller );
}
}
TestSuite
类派生自CppUnit::TestComposite
。
您可以创建多个测试套件,并在单个操作中使用TextTestRunner
对象运行它们。 您所需要做的就是像清单16中那样创建每个测试套件,然后将相同的addTest
方法添加到TextTestRunner
,如清单18所示。
CppUnit::TestSuite* suite1 = new CppUnit::TestSuite("mystringTest");
suite1->addTest(…);
…
CppUnit::TestSuite* suite2 = new CppUnit::TestSuite("mymathTest");
…
suite2->addTest(…);
CppUnit::TextTestRunner runner;
runner.addTest(suite1);
runner.addTest(suite2);
…
到目前为止,测试的输出已由TextTestRunner
类默认生成。 但是,CppUnit允许您在输出上使用自定义格式。 这样做的类之一是CompilerOutputter
,它在标头CompilerOutputter.h中声明。 其中,此类可让您指定用于在输出中显示文件名行号信息的格式。 另外,您可以将日志直接保存在文件中,而不是将其转储到屏幕上。 清单19提供了将输出转储到文件中的示例。 观察格式%p:%l
:前者表示文件的路径,而后者则显示行号。 使用此格式时,典型输出将类似于/home/arpan/work/str.cc:26。
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/CompilerOutputter.h>
int main ()
{
CppUnit::Test *test =
CppUnit::TestFactoryRegistry::getRegistry().makeTest();
CppUnit::TextTestRunner runner;
runner.addTest(test);
const std::string format("%p:%l");
std::ofstream ofile;
ofile.open("run.log");
CppUnit::CompilerOutputter* outputter = new
CppUnit::CompilerOutputter(&runner.result(), ofile);
outputter->setLocationFormat(format);
runner.setOutputter(outputter);
runner.run();
ofile.close();
return 0;
}
CompilerOutputter
还有许多其他有用的方法,例如printStatistics
和printFailureReport
,您可以使用它们来获取转储的全部信息的一部分。
到目前为止,您一直使用TextTestRunner
作为运行测试的默认值。 模式非常简单:实例化一个TextTestRunner
类型的对象,向其添加测试和输出,然后调用run
方法。 现在,通过使用TestRunner
( TextTestRunner
的超类)和称为(非常合适) 侦听器的新类类别来偏离此流程。 假设您打算跟踪每个测试花费的时间-这对于进行性能基准测试的开发人员来说是很普遍的事情。 在进行任何进一步的解释之前,请看清单20 。 该代码使用三个类: TestRunner
, TestResult
和myListener
,它们是从TestListener
派生的。 您使用清单10中相同的mystringTest
类。
class myListener : public CppUnit::TestListener {
public:
void startTest(CppUnit::Test* test) {
std::cout << "starting to measure time\n";
}
void endTest(CppUnit::Test* test) {
std::cout << "done with measuring time\n";
}
};
int main ()
{
CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
&mystringTest::checkLength));
suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
&mystringTest::checkLength));
CppUnit::TestRunner runner;
runner.addTest(suite);
myListener listener;
CppUnit::TestResult result;
result.addListener(&listener);
runner.run(result);
return 0;
}
[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time
myListener
类是CppUnit::TestListener
子类。 您需要相应地覆盖startTest
和endTest
方法,它们将分别在每个测试之前和之后执行。 您可以轻松地扩展这些方法,以检查各个测试所花费的时间。 那么,为什么不在设置/拆卸例程中添加此功能呢? 您可以,但这意味着在每个测试套件的设置/拆卸方法中复制代码。
接下来,查看运行器对象,它是TestRunner
类的实例,该类又在run
方法中采用TestResult
类型的参数,并将侦听器添加到TestResult
对象。
最后,您的输出发生了什么? TextTestRunner
在run
方法之后一直显示很多信息,但是TestRunner
却不执行任何操作。 您需要一个输出对象,该对象显示在测试执行期间侦听器对象收集的信息。 清单22显示了需要从清单20进行更改的内容。
runner.run(result);
CppUnit::CompilerOutputter outputter( &listener, std::cerr );
outputter.write();
但是,等等:这也不足以编译代码。 CompilerOutputter
的构造函数期望一个类型为TestResultCollector
的对象,并且由于TestResultCollector
本身是从TestListener
派生的( 有关详细信息,请参阅TestListener
中的CppUnit类层次结构的链接),因此您要做的就是从TestResultCollector
派生myListener
。 清单23显示了该编译。
class myListener : public CppUnit::TestResultCollector {
…
};
int main ()
{
…
myListener listener;
CppUnit::TestResult result;
result.addListener(&listener);
runner.run(result);
CppUnit::CompilerOutputter outputter( &listener, std::cerr );
outputter.write();
return 0;
}
输出如清单24所示。
[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time
str.cc:31:Assertion
Test name: checkLength
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero
str.cc:31:Assertion
Test name: checkValue
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero
Failures !!!
Run: 0 Failure total: 2 Failures: 2 Errors: 0
本文重点介绍CppUnit框架的某些特定类: TestResult
, TestListener
, TestRunner
, CompilerOutputter
等。 作为独立的单元测试框架,CppUnit提供了更多功能。 CppUnit中有一些类可用于XML输出生成( XMLOutputter
)和以GUI模式运行测试( MFCTestRunner
和QtTestRunner
),以及一个插件接口( CppUnitTestPlugIn
)。 确保进一步了解CppUnit文档的类层次结构以及安装随附的示例。
翻译自: https://www.ibm.com/developerworks/aix/library/au-ctools2_cppunit/index.html