本文转载自Alex's Blog http://www.zshalex.com/blog/?p=127
在Qt中编写单元测试的基本流程:
基于以上的原因,我们必须要在一个程序中运行所有的测试类,那应该如何实现这一功能呢?
既然运行单个测试是通过使用QTEST_MAIN宏来实现,那么我们就首先来看一下QTEST_MAIN宏的内容:
#define QTEST_MAIN(TestObject) \ int main(int argc, char *argv[]) \ { \ QApplication app(argc,argv); \ QTEST_DISABLE_KEYPAD_NAVIGATION \ TestObject tc; \ return QTest::qExec(&tc,argc,argv); \ }将宏展开后,它里面做的其实就是创建一个我们传入的测试类的实例,并调用QTest::qExec方法。既然如此,如果我们调用两次QTest::qExec,单元测试是否会运行两次呢?
仔细阅读Qt的文档,其中对QTest::qExec是这样描述的,如果运行以下的代码,那么MyFirstTestObject和MySecondTestObject都会被运行。
MyFirstTestObject test1; QTest::qExec(&test1); MySecondObject test2; QTest::qExec(&test2);但是QTest::qExec方法是不可重入的,同一时间只有一个测试可以被运行,就算是在不同的线程运行也是不被允许的。
OK现在我们已经确定可以在一个程序中运行多个单元测试,但是我们不可能在每次创建了一个测试类后,都手动的在main函数中添加运行测试的代码。如果手动添加,我们就会在mian函数中看到如下代码:
MyFirstTestObject test1; QTest::qExec(&test1); MySecondObject test2; QTest::qExec(&test2); MySecondObject test3; QTest::qExec(&test3); . . . MySecondObject testn; QTest::qExec(&testn);很显然这种方式也是非常不合理的,但是Qt的测试框架又不像CppUnit提供了TestSuite的功能,所以我们需要对Qt的测试框架进行扩展。
我们所扩展的测试框架有一下几个核心的需求:
运行测试主要分为两个步骤,首先要创建测试类的实例,然后调用QTest::qExec运行测试。
要想自动运行所有的测试,我们就需要一个列表来存放所有测试类的实例。另外由于要能自由的选择或排除某些单元测试,所以需要为每个测试类提供一个名称,以便之后的查找,所以可以使用Map去保存测试类,而不是列表。因此我们首先创建一个用于管理Map的类TestSuite。它的借口定义如下:
class TestSuite { public: static TestSuite* instance(); static void release(); TestSuite(); ~TestSuite(); void add(const QString &name, QObject *obj); int runTest(int argc, char** argv); const int totalTest(){return m_TotalTest;} const int errorCount(){return m_ErrorCount;} const int succCount(){return m_SuccCount;} const int runCount(){return m_RunCount;} const QStringList &errorTest(){return m_ErrorTest;} protected: void loadArg(int argc, char** argv); private: static TestSuite *m_TestSuite; //单元测试类的map QMap<QString, QObject*> map; //单元测试的总数 int m_TotalTest; //错误单元测试的个数 int m_ErrorCount; //正确单元测试的个数 int m_SuccCount; //运行的单元测试个数 int m_RunCount; //错误单元测试的名称列表 QStringList m_ErrorTest; //需要执行的单元测试的名称列表 QStringList m_IncludeTest; //不需要执行的单元测试的名称列表 QStringList m_ExcludeTest; //最终需要运行的单元测试的名称列表 QStringList m_RunTest; //getopt的配置 static const char *optString; static const struct option longOpts[]; };
其中最重要的两个方法是add与runTest。add会将指定的测试类的实例添加到map中,键值为name。runTest则会根据命令行参数运行指定的测试类。而命令行参数的分析使用getopt来分析,并在loadArg方法中实现。
另外TestSuite类使用了单体模式,可以通过TestSuite::instance()直接获取TestSuite类的实例。有了TestSuite类后,main函数中就只需要执行如下的代码,就能够自动运行所有的单元测试。
int main(int argc, char** argv) { QApplication app(argc, argv); int result = TestSuite::instance()->runTest(argc,argv) ; TestSuite::instance()->release(); return result; }
现在我们已经可以运行所有在map中的测试类,但是我们又要如何将测试类的实例添加map中呢?在这里我们参考了CppUnit的实现,在每个测试类中添加一个静态的指向当前类对象的指针,并在静态变量初始化时调用TestSuite类的add方法。
因此我们实现了一个TestCase类,并声明了两个宏,分别是DECLARE_TEST_CASE以及INIT_TEST_CASE。他们的实现如下:
//type表示类名 #define DECLARE_TEST_CASE(type) \ static type *tc; //type表示类名,name表示单元测试的名称 #define INIT_TEST_CASE(type,name) \ type *type::tc = new type(name); class TestCase : public QObject { Q_OBJECT public: explicit TestCase(const QString &name) { TestSuite::instance()->add(name,this); } };当我们要添加一个单元测试时,首先从TestCase类下继承一个类,例如:VideoTest。然后在头文件的public段中使用DECLARE_TEST_CASE宏去声明一个静态的变量,然后在cpp文件中使用INIT_TEST_CASE宏初始化静态变量。
//videotest.h class VideoTest : public TestCase { Q_OBJECT public: DECLARE_TEST_CASE(VideoTest); GetVideoRequestTest(const QString &name); }; //videotest.cpp INIT_TEST_CASE(VideoTest,"videoTest"); VideoTest::VideoTest(const QString &name): TestCase(name) { }
这样我们创建的测试类就会自动添加到TestSuite的map中,之后只需要像之前一样编写测试函数就可以了。