构建测试包
前言
我们要分析的源码样本就是simple工程,因为这个工程顾名思义,肯定是最简单的嘛。这个工程里的源码主要包括两个部分:
- main函数
- ExampleTestCase类
main函数比较复杂,我们先跳过这部分,先看ExampleTestCase类。
ExampleTestCase类
这个类派生自TestFixture,上一章我们已经说过了,这种方式自定义测试类比较简单。先看看基类的声明:
{
public:
virtual ~TestFixture() {};
//! \brief Set up context before running a test.
virtual void setUp() {};
//! Clean up after the test run.
virtual void tearDown() {};
};
比较简单,声明了两个虚函数setUp和tearDown,默认实现是空。上一章我们已经说过了,这两个函数负责测试用例一些公共资源的初始化和清理。
下面我们看看ExampleTestCase类的声明:
{
CPPUNIT_TEST_SUITE( ExampleTestCase );
CPPUNIT_TEST( example );
CPPUNIT_TEST( anotherExample );
CPPUNIT_TEST( testAdd );
CPPUNIT_TEST( testDivideByZero );
CPPUNIT_TEST( testEquals );
CPPUNIT_TEST_SUITE_END();
protected:
double m_value1;
double m_value2;
public:
void setUp();
protected:
void example();
void anotherExample();
void testAdd();
void testDivideByZero();
void testEquals();
};
我们看到,这个类还是比较简单的,其中的setUp函数重载了,查一下实现:
{
m_value1 = 2.0;
m_value2 = 3.0;
}
仅仅是初始化两个成员变量。基类的另一个虚函数tearDown没有重载,还是空。
再看看其它几个自定义的测试函数,都比较简单。
真正有问题的是开头的那几个宏,这几个宏负责构建我们的测试包,本章将详细的讲述这几个宏,揭示它们背后的秘密。
三个宏定义
CPPUNIT_TEST_SUITE
切换到CPPUNIT_TEST_SUITE的宏定义上,真是超长的宏定义啊:
2 public: \
3 typedef ATestFixtureType TestFixtureType; \
4 \
5 private: \
6 static const CPPUNIT_NS::TestNamer &getTestNamer__() \
7 { \
8 static CPPUNIT_TESTNAMER_DECL( testNamer, ATestFixtureType ); \
9 return testNamer; \
10 } \
11 \
12 public: \
13 typedef CPPUNIT_NS::TestSuiteBuilderContext<TestFixtureType> \
14 TestSuiteBuilderContextType; \
15 \
16 static void \
17 addTestsToSuite( CPPUNIT_NS::TestSuiteBuilderContextBase &baseContext ) \
18 { \
19 TestSuiteBuilderContextType context( baseContext )
不过看多了MFC消息映射的宏定义,这东西其实也没啥难度,或许,原本这东西就是从MFC那里学来的。
- 代码行3,首先把我们自己的测试用例ExampleTestCase重新声明为TestFixtureType类型,就是变个名称而已,使宏看起来好看一点
- 代码行6,然后,声明一个静态的私有函数getTestNamer__,得到测试用例的名字
- 代码行8,这个函数里面又是一个宏:
#if CPPUNIT_USE_TYPEINFO_NAME
# define CPPUNIT_TESTNAMER_DECL( variableName, FixtureType ) \
CPPUNIT_NS::TestNamer variableName( typeid(FixtureType) )
#else
# define CPPUNIT_TESTNAMER_DECL( variableName, FixtureType ) \
CPPUNIT_NS::TestNamer variableName( std::string(#FixtureType) )原来不过是声明一个变量而已,变量类型是TestNamer,构造函数的参数就是"ExampleTestCase"
- 代码行13-14,把TestSuiteBuilderContext类型重新声明为TestSuiteBuilderContextType,又是变个名称而已,但是TestSuiteBuilderContext又是什么东西?从名称上来看,很明显,这 里采用了Builder设计模式,先放放,回头我们再细看。
- 代码行16-17,再声明一个静态的公有函数addTestsToSuite,加入测试用例到测试组,参数是什么?就是一个TestSuiteBuilderContextBase,看起来这家伙 就是Builder设计模式中所有ConcreteBuilder的基类了
- 代码行19,声明了一个对象context,类型是一个ConcreteBuilder,参数就是baseContext,ConcreteBuilder的基类,注意,这个函数没有实现完嘛,因为没有结束的},很明显,这是为了需要后续的宏继续写下去
总结一下,这个宏用getTestNamer__函数注册了自定义测试类的名称作为标识,然后用addTestsToSuite函数开始注册自定义测试函数
CPPUNIT_TEST
切换到CPPUNIT_TEST的宏定义上:
2 CPPUNIT_TEST_SUITE_ADD_TEST( \
3 ( new CPPUNIT_NS::TestCaller<TestFixtureType>( \
4 context.getTestNameFor( #testMethod), \
5 &TestFixtureType::testMethod, \
6 context.makeFixture() ) ) )
- 代码行2,先看CPPUNIT_TEST_SUITE_ADD_TEST的宏定义
#define CPPUNIT_TEST_SUITE_ADD_TEST( test ) \
context.addTest( test )还好,这句好懂,就是给上下文加一个测试用例嘛。
- 代码行3,就是new一个TestCaller
而已 ,这个类我们上一章说过一点,这是一个辅助类,但是它到底是如何起作用的呢?待查 - 代码行4,又是一个名称相关的东西,一个‘#’号就暴露无疑了,这里传入了测试参数的名称字符串
- 代码行5,一个指向成员函数的指针,还好,没忘了这么偏的语法,不过想想也是,这里很明显是要注册每个自定义测试函数的信息嘛,传入一个函数名称用于标识,传入一个函数指针用于执行
- 代码行6,这里让context上下文创建测试装置
总结一下,这个宏用TestCaller类注册了一个测试函数的信息,包括函数名称和函数指针,然后再加入到context上下文 中。
CPPUNIT_TEST_SUITE_END
切换到CPPUNIT_TEST_SUITE_END的宏定义上:
2 } \
3 \
4 static CPPUNIT_NS::TestSuite *suite() \
5 { \
6 const CPPUNIT_NS::TestNamer &namer = getTestNamer__(); \
7 std::auto_ptr<CPPUNIT_NS::TestSuite> suite( \
8 new CPPUNIT_NS::TestSuite( namer.getFixtureName() )); \
9 CPPUNIT_NS::ConcretTestFixtureFactory<TestFixtureType> factory; \
10 CPPUNIT_NS::TestSuiteBuilderContextBase context( *suite.get(), \
11 namer, \
12 factory ); \
13 TestFixtureType::addTestsToSuite( context ); \
14 return suite.release(); \
15 } \
16 private: /* dummy typedef so that the macro can still end with ';'*/ \
17 typedef int CppUnitDummyTypedefForSemiColonEnding__
我再晕,又是这么长的一个宏啊。
- 代码行2,终于看到addTestsToSuite函数结束的标志了
- 代码行4,一波未平一波又起啊,又搞出来一个suite函数,返回测试包对象的指针
- 代码行6,首先得到测试类的名字,这个函数我们刚才看过了
- 代码行7-8,然后用这个测试类的名字构造一个测试包的对象,这里用到了智能指针,当然是为了一旦有问题不用手动delete了
- 代码行9,一个ConcretTestFixtureFactory
,一个测试类的工厂 ,注意这里是模板类,类型就是自定义测试类,居然是工厂设计模式,看样子应该是工厂方法设计模式吧,待查 - 代码行10-12,咦,前面看到过的Builder基类怎么又来凑热闹了,不过也别说,这家伙要干的事情也真是麻烦,参数这么多,他要求有测试包对象,测试包名称,测试类工厂
- 代码行13,addTestsToSuite,好熟悉的名字啊,原来就是第一个宏里面的函数嘛,原来这里就是它调用的地方啊
- 代码行14,release了,返回测试包的指针,智能指针清空了
- 代码行17,基本上这就是占位子用的,名字起得也很清楚,这是为了让编译器检查写宏的时候别忘了写那个‘;’,这个技巧倒是挺好玩的
总结一下,这个宏只有一个函数suite,用于创建测试包,这个函数不仅仅是addTestsToSuite函数的结束,同时也是addTestsToSuite调用的地方。
总结
宏CPPUNIT_TEST_SUITE,CPPUNIT_TEST,CPPUNIT_TEST_SUITE_END,这三个宏 彼此配合,形成了以下模式:
CPPUNIT_TEST( 成员函数 );
CPPUNIT_TEST( 成员函数 );
CPPUNIT_TEST( 成员函数 );
。。。
CPPUNIT_TEST_SUITE_END();
其中注册了自定义的测试类信息,注册了所有自定义的测试函数信息,最后通过一个suite函数创建了一个测试包的对象。
遗留问题
上一节,我们初步了解了CppUnit通过一组宏注册自定义测试类和自定义测试函数,并创建测试包对象的全过程。但是其中还留下了一些遗留问题,本节将彻底为你揭开剩下的谜团。
构建测试包
在构建测试包的过程中,有一个类起到了关键的作用,这就是TestSuiteBuilderContext,这是一个模板类,从TestSuiteBuilderContextBase派生,查看了这两个类的声明后,我们得到了下面的类图:
居于核心位置的TestSuiteBuilderContextBase同时聚合了TestSuite和TestFixtureFactory两个类的对象,实际上它更象是一个中间联络人,让我们看一下它的函数实现:
{
m_suite.addTest( test );
}
TestFixture * TestSuiteBuilderContextBase::makeTestFixture() const
{
return m_factory.makeFixture();
}
都是直接转交给相应的类对象。
TestSuiteBuilderContextBase中还有一个成员变量值得注意:
typedef CppUnitVector<Property> Properties;
private:
Properties m_properties;
很明显,这里用字符串的形式记录了测试包创建过程中的所有属性。
我们再来看子类:
class TestSuiteBuilderContext : public TestSuiteBuilderContextBase
{
public:
typedef Fixture FixtureType;
TestSuiteBuilderContext( TestSuiteBuilderContextBase &contextBase )
: TestSuiteBuilderContextBase( contextBase )
{
}
/*! \brief Returns a new TestFixture instance.
* \return A new fixture instance. The fixture instance is returned by
* the TestFixtureFactory passed on construction. The actual type
* is that of the fixture on which the static method suite()
* was called.
*/
FixtureType *makeFixture() const
{
return CPPUNIT_STATIC_CAST( FixtureType *,
TestSuiteBuilderContextBase::makeTestFixture() );
}
};
注意其中的makeFixture函数,就是直接调用基类的makeTestFixture函数,并根据实际类型进行转型,而我们知道基类的makeTestFixture函数的实现是直接转交给相应工厂的。
那么我们再看看这个工厂,都是很简单的代码:
*
* Implementation detail. Use by HelperMacros to handle TestFixture hierarchy.
*/
class TestFixtureFactory
{
public:
//! Creates a new TestFixture instance.
virtual TestFixture *makeFixture() =0;
};
/*! \brief Concret TestFixture factory (Implementation).
*
* Implementation detail. Use by HelperMacros to handle TestFixture hierarchy.
*/
template<class TestFixtureType>
class ConcretTestFixtureFactory : public CPPUNIT_NS::TestFixtureFactory
{
/*! \brief Returns a new TestFixture instance.
* \return A new fixture instance. The fixture instance is returned by
* the TestFixtureFactory passed on construction. The actual type
* is that of the fixture on which the static method suite()
* was called.
*/
TestFixture *makeFixture()
{
return new TestFixtureType();
}
};
了解了这些,再回过头来看suite函数的实现就很简单了。
注册测试函数
在注册测试函数的过程中,也有一个类起到了关键的作用,它就是TestCaller,这也是一个模板类。关于这个类,它的类图是:
模板的实参就是我们的自定义测试类ExampleTestCase,所以TestCaller其实是一个辅助类,负责把ExampleTestCase和TestCase关联起来。让我们看看它的函数实现:
void runTest()
{
// try {
(m_fixture->*m_test)();
// }
// catch ( ExpectedException & ) {
// return;
// }
// ExpectedExceptionTraits<ExpectedException>::expectedException();
}
void setUp()
{
m_fixture->setUp ();
}
void tearDown()
{
m_fixture->tearDown ();
}
很明显,这个类是把基类TestCase要完成的工作全都原封不动就转交给我们自己的自定义测试类ExampleTestCase了,原来我们就是把这样一个偷懒的家伙加入到我们的测试包中了,本身TestCaller的类型就是TestCase,类型没有问题,但是干活的时候却没它什么事情。
注意runTest函数的实现,用函数指针的语法,一次执行一个自定义测试函数。但是这个函数是如何被调用的呢?这话说起来就长了,我们留给下回分解吧。