简单测试用例(Simple Test Case)
如果你想知道你的代码是否运行正确,你该怎么做?有很多方法。通过debugger单步调试或者给你的代码里面乱填一些输出流是两种最简单的方法,但是它们都有缺点。单步调试虽然好但不能自动完成,每当你的代码变化时都不得不重新来。输出文本的方法也好,但是它会把代码弄得很不好看,并且大多数情况下,产生比你需要多得多的信息。
用CppUnit测试能够自动运行。这种测试很容易创建,并且一旦你写好这些测试,它们就一直帮助你保证代码质量。
下面先介绍一下一个简单用例的创建:
写一个TestCase的派生类,重载runTest()方法。当你想核对一个数值时,调用CPPUNIT_ASSERT(bool) ,传一个判断表达式,如果为真,测试成功。
例如,测试复数类的等值比较,可以写为:
class ComplexNumberTest : public CppUnit::TestCase {
public:
ComplexNumberTest( std::string name ) : CppUnit::TestCase( name ) {}
void runTest() {
CPPUNIT_ASSERT( Complex (10, 1) == Complex (10, 1) );
CPPUNIT_ASSERT( !(Complex (1, 1) == Complex (2, 2)) );
}
};
这是个非常简单的测试。通常你会对同一个对象运行很多小测试,你可以选择fixture来完成。
Fixture
Fixture是一些已知对象的集合,是更多的测试用例集合的基础,在开发的时候非常有用。
让我们在开发形式下学习Fixture。假设我们正在开发复数类,首先以定义一个空的Complex类。
class Complex {};
现在为上面创建一个ComplexNumberTest 测试用例,编译代码看会发生什么情况。首先注意到的是一些编译错误。测试用到了未定义的操作符 == ,现在把它定义如下:
bool operator==( const Complex &a, const Complex &b)
{
return true;
}
现在编译运行这个测试,这次通过了编译但是测试失败。我们需要多做点事情让操作符== 正确工作,因此重新写的代码如下:
class Complex {
friend bool operator ==(const Complex& a, const Complex& b);
double real, imaginary;
public:
Complex( double r, double i = 0 )
: real(r), imaginary(i)
{
}
};
bool operator ==( const Complex &a, const Complex &b )
{
return a.real == b.real && a.imaginary == b.imaginary;
}
现在再编译运行的话,我们的测试就能够通过了。
现在已经准备好添加新的操作和新的测试了,此时fixture变得非常的方便。如果我们使用三四个复数对象,重用它们并通过所有测试,那会有多好。
下面是我们的做法:
l 给fixture添加成员变量;
l 重写setUp()初始化这些变量;
l 重写tearDown()去释放setUp()中分配的所有永久性资源。
class ComplexNumberTest : public CppUnit::TestFixture {
private:
Complex *m_10_1, *m_1_1, *m_11_2;
public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}
void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}
};
一旦有了这个fixture, 我们可以添加复杂的加法测试用例和开发过程中需要的其他测试用例。
测试用例(Test Case)
怎样用fixture编写和调用单个测试?
这里是它的两个步骤:
l 在fixture类中,把测试用例编写成一个函数;
l 创建TestCaller来运行这个函数。
下面是我们的测试用例,其中有一些附加的用例函数:
class ComplexNumberTest : public CppUnit::TestFixture {
private:
Complex *m_10_1, *m_1_1, *m_11_2;
public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}
void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}
void testEquality()
{
CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
}
void testAddition()
{
CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
}
};
可以像下面这样来创建运行每个测试用例的实例:
CppUnit::TestCaller<ComplexNumberTest> test( "testEquality", &ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );
TestCaller构造函数中的第二个参数是ComplexNumberTest中测试函数的地址。当TestCaller对象运行时,指定的函数将被执行。但是,因为没有诊断信息,它还没有实际的用处。通常用TestRunner(下面有介绍)来显示结果。
如果你有多个测试需要做的话,可以把它们组织成为一个suite。
Suite
怎么创建你的多个测试,使它们一次性运行呢?
CppUuite 提供了一个TestSuite类可以使任意多数目的TestClass一起运行。
上面我们看到了怎样运行单个的测试用例,下面创建一个有两个或更多的测试的suite:
CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suite.addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
suite.run( &result );
TestSuites必须包含测试用例(TestCases)的调用者(caller),实际上它能包含任意实现了Test接口的对象。比方说,你在自己的代码中创建一个TestSuite,我在自己的代码中也创建一个,那么我们能够通过创建一个TestSuite包含它们俩,把它们放在一起运行。代码可以写成下面的样子:
CppUnit::TestSuite suite;
CppUnit::TestResult result;
suite.addTest( ComplexNumberTest::suite() );
suite.addTest( SurrealNumberTest::suite() );
suite.run( &result );
TestRunner
如何运行测试程序并收集它们的结果?
一旦你有个测试suite,你就会运行它。CppUnit提供了一个定义要运行的Suite并显示结果的工具。你可以通过定义一个静态的suite函数,返回一个测试suite,使TestRunner能访问你的suite。
例如,为使ComplexNumberTest可以被TestRunner 访问,在ComplexNumberTest中添加如下代码:
public:
static CppUnit::Test *suite()
{
CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );
suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testEquality",
&ComplexNumberTest::testEquality ) );
suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
"testAddition",
&ComplexNumberTest::testAddition ) );
return suiteOfTests;
}
然后在Main.cpp中包含测试的头文件,如下:
#include <cppunit/ui/text/TestRunner.h>
#include "ExampleTestCase.h"
#include "ComplexNumberTest.h"
并要在main()函数中添加addTest(CppUnit::Test *)的调用:
int main( int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );
runner.addTest( ComplexNumberTest::suite() );
runner.run();
return 0;
}
TestRunner 将会运行这些测试。若所有测试通过,将得到一个报告消息;若任何一个测试失败,将会收到如下信息:
l 失败的测试用例名称;
l 包含这些测试的源文件名;
l 失败发生地方的行数;
l 检测失败的CPPUNIT_ASSERT()断言里面的信息。
CppUnit区分失败和错误。失败可以用断言来预测和检查。错误不能预测,比如被零除和C++运行时间或本身代码抛出的异常。
Helper Macros
你可能已经注意到,实现fixture静态suite()函数是一个重复的且容易出错的工作。一些Writing test fixture的宏集合可以用于自动实现静态suite()函数。
下面是用这些宏重写的ComplexNumberTest代码:
#include <cppunit/extensions/HelperMacros.h>
class ComplexNumberTest : public CppUnit::TestFixture {
首先,声明suite,把类名传给宏:
CPPUNIT_TEST_SUITE( ComplexNumberTest );
静态suite()函数创建的suite按类取名。然后,声明fixture的每一个测试用例:
CPPUNIT_TEST( testEquality );
CPPUNIT_TEST( testAddition );
最后,结束suite的声明:
CPPUNIT_TEST_SUITE_END();
这时,带有以下声明(signature)的方法已经实现:
static CppUnit::TestSuite *suite();
fixture的其他部分保持不变:
rivate:
Complex *m_10_1, *m_1_1, *m_11_2;
public:
void setUp()
{
m_10_1 = new Complex( 10, 1 );
m_1_1 = new Complex( 1, 1 );
m_11_2 = new Complex( 11, 2 );
}
void tearDown()
{
delete m_10_1;
delete m_1_1;
delete m_11_2;
}
void testEquality()
{
CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
}
void testAddition()
{
CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
}
};
添加到suite的TestCaller 的名字是fixture和方法名称的合成。目前用例中,名字将叫做“ComplexNumberTest.testEquality”和“ComplexNumberTest.testAddition”。
Helper Macros能帮助你写普通的断言,例如,当被零除的时候,复数(ComplexNumber)抛出MathException异常:
l 指定异常的类型,用CPPUNIT_TEST_EXCEPTION把异常测试添加到suite中;
l 写出测试用例方法。
CPPUNIT_TEST_SUITE( ComplexNumberTest );
// [...]
CPPUNIT_TEST_EXCEPTION( testDivideByZeroThrows, MathException );
CPPUNIT_TEST_SUITE_END();
// [...]
void testDivideByZeroThrows()
{
// The following line should throw a MathException.
*m_10_1 / ComplexNumber(0);
}
若预计的异常没有抛出,将会有断言失败的报告。
TestFactoryRegistry
TestFactoryRegistry用来解决以下两个问题:
l 忘记将你的fixture suite添加到test runner中,(因为它在另外一个文件中,很容易被遗忘)
l 所有测试用例头文件的包含会引进编辑的瓶颈(参看以前的例子)
TestFactoryRegistry是一个suite在初始化时被注册的地方,在.cpp文件中注册ComplexNumber suite,你可以添加:
#include <cppunit/extensions/HelperMacros.h>
CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumberTest );
在幕后,一个AutoRegisterSuite类型的静态变量被声明。在构造函数里,它将一个TestSuiteFactory注册到TestFactoryRegistry里。TestSuiteFactory用来返回函数ComplexNumber::suite()返回的TestSuite对象。
用下面test runner 代码运行测试,我们就不再需要包含fixture:
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>
int main( int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
首先,我们得到TestFactoryRegistry的实例:
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
然后,获得和添加由TestFactoryRegistry创建的新的TestSuite,其中包括通过CPPUNIT_TEST_SUITE_REGISTRATION()注册的所有Test suite:
runner.addTest( registry.makeTest() );
runner.run();
return 0;
}
Post-build check
现在我们的单元测试已经运行了,怎么把单元测试整合到build process中呢?为此,应用程序必须返回一个不等于0的数值来标识这里出错了。TestRunner::run()返回一个bool变量来标识运行是否成功。更新我们的主程序如下:
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>
int main( int argc, char **argv)
{
CppUnit::TextUi::TestRunner runner;
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
runner.addTest( registry.makeTest() );
bool wasSuccessful = runner.run( "", false );
return !wasSuccessful;
}
现在,在编译之后运行你的应用程序。
对于Visual C++,在Project Settings/Post-Build中,通过添加“$(TargetPath)”命令来完成这个步骤。它扩展到应用程序可执行路径中,参考以下项目,它用了这个方法:
examples/cppunittest/CppUnitTestMain.dsp
源自Michael Feathers,由Baptiste Lepilleur维护Doxygen和更新,戴诏翻译。