本文转载自Alex's Blog
原文链接已不存在,如有侵权,请联系我删除
在Qt中编写单元测试的基本流程:
- 创建一个类,并使其从QObject类继承下来。
- 编写测试函数。
- 使用QTEST_MAIN宏运行测试。
但是在以上的流程中存在一个问题,在实际的项目中,我们不可能对每个测试类都使用QTEST_MAIN宏去运行。因为如果这样做,就意味着每个测试类都会有一个与它相对应的可执行程序,这显然是不合理,既不方便我们编写单元测试,也不方便之后的数据统计。
基于以上的原因,我们必须要在一个程序中运行所有的测试类,那应该如何实现这一功能呢?
既然运行单个测试是通过使用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中,之后只需要像之前一样编写测试函数就可以了。