引言
在《Google C++单元测试框架(Gtest)系列教程之二——断言、函数测试》中,我们了解了断言语句,以及如何运用TEST()进行函数测试,在TEST()的使用中,我们接触了一个测试用例包含多个测试实例的组织方式。多个测试实例可能需要进行相识的数据配置和初始化操作,为此,Gtest提供了测试固件(Test fixture)帮助我们进行数据管理。
“落后”的方法
在了解测试固件之前,我们先来看以下测试例子:
template <typename E> // E is the element type.
class Queue {
public:
Queue();
void Enqueue(const E& element);
E* Dequeue(); // Returns NULL if the queue is empty.
size_t size() const;
...
};
假设我们要对以上Queue类进行测试,根据我们之前学习到的TEST()的用法,编写测试代码如下:
//测试方案一
TEST(QueueTest, IsEmptyInitially) {
Queue<int> q0_;
EXPECT_EQ(0, q0_.size());
}
TEST(QueueTest, DequeueWorks) {
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(2, *n);
EXPECT_EQ(1, q2_.size());
delete n;
}
不知你是否已经发现问题所在呢?对,红色字体的测试数据初始化部分存在重复代码!在该例子中仅包含两个测试实例,重复代码的问题并不突出,但对于几十个甚至上百个测试实例而言,我们就需要另一种方式管理我们的初始化数据了。
测试固件(Test fixture)
测试固件的作用在于管理两个或多个测试实例都会使用到的数据,使用测试固件完成上述测试,方法如下:
首先我们需要定义一个固件类(fixture class),一般固件类以FooTest的形式命名,其中Foo为被测类的名称:
class QueueTest : public ::testing::Test {
protected:
virtual void SetUp() {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// virtual void TearDown() {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
定义固件类的方法为:
- 写一个继承自::test::Test的类,为使该类的子类能访问到该类的数据,使用public或protected作为访问控制标识;
- 在该类中,定义测试实例将用到的数据;
- 使用SetUp()方法或默认构造函数作数据初始化操作,使用TearDown()方法或析构函数作数据清理操作,注意SetUp()和TearDown()的拼写;
- 如有需要,还可以在该类中定义成员函数,正如初始化数据,这里所定义的成员函数也可被测试实例重复使用。
接下来我们来看如何编写相应的测试实例,首先我们要用到一个新的宏:
TEST_F(test_case_name, test_name) {
... test body ...
}
TEST_F()必须在测试固件定义之后才能使用,其两个参数含义与TEST()的参数含义相同,但TEST_F()的第一个参数必须为固件类的名称。
结合上述QueueTest测试固件,我们编写测试代码如下:
//测试方案二
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(0, q0_.size());
}
TEST_F(QueueTest, DequeueWorks) {
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(2, *n);
EXPECT_EQ(1, q2_.size());
delete n;
}
可以看出TEST_F()的使用方法与TEST()差别不大,当以上两个测试实例运行时,Gtest为我们做了以下事情:
- 构造一个QueueTest对象(假设为t1);
- 调用t1.SetUp()初始化t1对象;
- 第一个测试实例(IsEmptyInitially)使用t1进行测试;
- 调用t1.TearDown()进行数据清理;
- 销毁对象t1;
- 创建一个新的QueueTest对象,对下一个测试实例DequeueWorks重复以上步骤。
可见Gtest通过创建和销毁固件类对象,为每一个测试实例创建了一份独立的初始化数据,上面的两个测试方案的目的和结果完全一样,但方案二通过使用测试固件,杜绝了数据初始化带来的重复代码。
固件类(Fixture class)
C++类具有可继承的特点,这样我们可以灵活地定义固件类,我们可以把多个固件类共有的特性抽象出来形成一个基类,以进一步达到代码复用、数据复用的效果,来看下面一个例子。
class QuickTest : public testing::Test {
protected:
// This is a good place to record the start time.
virtual void SetUp() {
start_time_ = time(NULL);
}
// check if the test was too slow.
virtual void TearDown() {
// Gets the time when the test finishes
const time_t end_time = time(NULL);
// Asserts that the test took no more than ~5 seconds.
EXPECT_TRUE(end_time - start_time_ <= 5) << "The test took too long.";
}
该固件类对测试实例的运行时间作一个简单的分析,其利用了SetUp()在测试实例运行前执行、TearDown()在测试实例运行后执行的特点,运行时间超过5秒的测试实例将检测失败,注意SetUp()和TearDown()函数中也可以使用断言语句。
假设我们对Queue类的测试实例有执行时间限制,我们可以编写继承自QuickTest的固件类:
class QueueTest : public QuickTest {
//......
};
经过这样定义,与QueueTest相关联的测试实例运行时,其执行时间将得到检测。
小结
本文介绍了使用Gtest测试固件(Test fixture)的原因及方法,最后提出可以通过类继承的方式灵活定义测试固件。下一节将介绍Gtest值参数化、类型参数化的使用方法。
Reference: googletest project