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

引言

《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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值