代码改变世界

Google C++单元测试框架(Gtest)系列教程之三——测试固件(Test fixture)

2011-10-05 00:36  bangerlee  阅读(12348)  评论(4编辑  收藏  举报

引言

《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_;
};

定义固件类的方法为:

  1. 写一个继承自::test::Test的类,为使该类的子类能访问到该类的数据,使用public或protected作为访问控制标识;
  2. 在该类中,定义测试实例将用到的数据;
  3. 使用SetUp()方法或默认构造函数作数据初始化操作,使用TearDown()方法或析构函数作数据清理操作,注意SetUp()和TearDown()的拼写;
  4. 如有需要,还可以在该类中定义成员函数,正如初始化数据,这里所定义的成员函数也可被测试实例重复使用。

接下来我们来看如何编写相应的测试实例,首先我们要用到一个新的宏:

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为我们做了以下事情:

  1. 构造一个QueueTest对象(假设为t1);
  2. 调用t1.SetUp()初始化t1对象;
  3. 第一个测试实例(IsEmptyInitially)使用t1进行测试;
  4. 调用t1.TearDown()进行数据清理;
  5. 销毁对象t1;
  6. 创建一个新的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