学习如何使用cpp工程如何使用GoogleTest框架编写测试。

GoogleTest这样定义了一个好的测试

  1. 测试独立并且可以重复的。即一个测试不会依赖其他测试。并且每个测试的结果是确定的。
  2. 测试要被良好地组织并且能够反映被测试代码结构。GoogleTest将有相关性的测试分为一组test suites,可以共享数据和辅助的函数。
  3. 测试需要可移植和重用。
  4. 测试失败要提供足够多的信息
  5. 测试框架要让编写测试的人只关注测试的内容。
  6. 测试要很快。google test可以在测试之间共享资源

GoogleTest术语注意事项

在google test框架中,术语test, test case分别对应ISTQB定义的test case , test suites。

不过google test也在使用test suites替代test case了,并提供了对应的api,旧的api将会过期。

在google test中有如下几个概念

  • Tests:使用断言来验证被测试代码的行为,如果测试崩溃或者断言失败,则测试通过;否则成功。
  • Test suite:包含一个或多个test。当test suite中的多个测试之间需要共享公共数据对象和辅助函数,可以将它们放在test fixture当中。
  • Test program:一个test program可以包含多个test suites。

编写简单的测试

使用TEST宏来编写测试代码,宏结构示例如下

TEST(TestSuiteName, TestName) {
  ... test body ...
}

TestSuiteName相同的Test属于同一个Test Suite,TestName则是具体这个测试的名称。 在内部使用断言来验证结果。断言失败则整个测试失败。两者都必须是有效的c++标识符,并且不能包含任何下划线。

TEST定义的测试全名由TestSuiteName和TestName组合而来。

GoogleTest提供了丰富的断言,可以参考官方文档Assertions

Test Fixtures:测试之间共享数据

Test suite中的不同测试想要使用共同的数据,就可以通过Test Fixtures来解决。

首先要创建一个fixture类,流程如下:

  1. 创建一个派生于testing::Test的类,所有的内容都放在protected下,因为需要自类能够访问到。
  2. 在这个类型生命要共享的对象
  3. 使用默认构造or SetUp()初始化好要共享的数据。
  4. 使用默认析构函数or TearDown()来释放资源
  5. 共享的辅助函数也可以添加到里面

使用TEST_F宏而非TEST宏来编写测试,这样在内部就可以使用上面创建的fixture中的共享数据了。宏结构如下

TEST_F(TestFixtureClassName, TestName) {
  ... test body ...
}

其中第一个参数必须是前面创建的fixture类的类名。 TEST_F宏定义的每个测试使用的都是一个新的fixture对象,并且立即执行SetUp进行初始化,然后再执行测试,接着执行TearDown清理资源,最后删除fxiture对象。

现有一段代码需要进行测试

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;
  ...
};

按照前面所说的流程,创建一个fixture

class QueueTest : public testing::Test {
 protected:
  QueueTest() {
     // q0_ remains empty
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }
 
  // ~QueueTest() override = default;
 
  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

这样在为Queue编写测试代码的时候,就可以直接使用QueueTest中的内容了。

TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0);
}
 
TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(n, nullptr);
 
  n = q1_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 1);
  EXPECT_EQ(q1_.size(), 0);
  delete n;
 
  n = q2_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 2);
  EXPECT_EQ(q2_.size(), 1);
  delete n;
}

运行测试

TESTTEST_F都会隐式将测试注册到GoogleTest当中。要执行这些测试则是需要执行另一个宏RUN_ALL_TESTS(),这个宏会执行连接单元中的所有测试。执行结果为0表示测试成功。 运行宏的执行流程是这样的

  1. 保存GoogleTest的flags状态
  2. 为第一个测试创建fixture
  3. 调用SetUp初始化
  4. 运行fixture上的测试
  5. 调用TearDown清理fixture
  6. 删除fixture
  7. 恢复GoogleTest的flags状态
  8. 重复上述流程直到所有的测试运行完成。

编写main函数

所有的测试用例最终都要编译成一个可执行文件进行执行,因此同样需要编写main函数。 上面运行测试使用的RUN_ALL_TESTS()宏就要在main函数中调用。

大多数情况用户也不必编写main函数,而是连接到gtest_main方法,这里定义了合适的入口点。当然如果需要在执行gtest做一些自定义内部,那么编写main方法还是有必要的。

main函数的样板代码如下

#include "this/package/foo.h"
 
#include <gtest/gtest.h>
 
namespace my {
namespace project {
namespace {
 
// The fixture for testing class Foo.
class FooTest : public testing::Test {
 protected:
  // You can remove any or all of the following functions if their bodies would
  // be empty.
 
  FooTest() {
     // You can do set-up work for each test here.
  }
 
  ~FooTest() override {
     // You can do clean-up work that doesn't throw exceptions here.
  }
 
  // If the constructor and destructor are not enough for setting up
  // and cleaning up each test, you can define the following methods:
 
  void SetUp() override {
     // Code here will be called immediately after the constructor (right
     // before each test).
  }
 
  void TearDown() override {
     // Code here will be called immediately after each test (right
     // before the destructor).
  }
 
  // Class members declared here can be used by all tests in the test suite
  // for Foo.
};
 
// Tests that the Foo::Bar() method does Abc.
TEST_F(FooTest, MethodBarDoesAbc) {
  const std::string input_filepath = "this/package/testdata/myinputfile.dat";
  const std::string output_filepath = "this/package/testdata/myoutputfile.dat";
  Foo f;
  EXPECT_EQ(f.Bar(input_filepath, output_filepath), 0);
}
 
// Tests that Foo does Xyz.
TEST_F(FooTest, DoesXyz) {
  // Exercises the Xyz feature of Foo.
}
 
}  // namespace
}  // namespace project
}  // namespace my
 
int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

testing::InitGoogleTest用于从命令行中解析处GoogleTest flags,可以用来控制测试程序的行为,这这下一小节进一步说明。

要是不需要对main方法做任何改动,那么直接链接gtest_main这个库即可,库里提供了默认的main函数实现。

编写MOCK测试

mock一个普通的class

现在有如下的类

class Foo {
 public:
  virtual ~Foo();
  virtual int GetSize() const = 0;
  virtual string Describe(const char* name) = 0;
  virtual string Describe(int type) = 0;
  virtual bool Process(Bar elem, int count) = 0;
};

我们要对其进行mock,需要注意的是,上面被mock的Foo的析构函数必须是虚函数。

这样可以为Foo创建一个子类,使用gmock的宏来mock需要的函数

#include <gmock/gmock.h>
 
class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, GetSize, (), (const, override));
  MOCK_METHOD(string, Describe, (const char* name), (override));
  MOCK_METHOD(string, Describe, (int type), (override));
  MOCK_METHOD(bool, Process, (Bar elem, int count), (override));
};

在上面的基础上,还可以进一步创建三种模式的Mock子类

using ::testing::NiceMock;
using ::testing::NaggyMock;
using ::testing::StrictMock;
 
NiceMock<MockFoo> nice_foo;      // 忽略不感兴趣的函数调用
NaggyMock<MockFoo> naggy_foo;    // 对不感兴趣的函数调用进行告警
StrictMock<MockFoo> strict_foo;  // 不感兴趣的函数调用会失败

mock一个类模板

模板类如下

template <typename Elem>
class StackInterface {
 public:
  virtual ~StackInterface();
  virtual int GetSize() const = 0;
  virtual void Push(const Elem& x) = 0;
};

同样的,析构函数函数也要是虚函数。

template <typename Elem>
class MockStack : public StackInterface<Elem> {
 public:
  MOCK_METHOD(int, GetSize, (), (const, override));
  MOCK_METHOD(void, Push, (const Elem& x), (override));
};

mock的流程和前面是一致的。

在test中使用mock类

使用流程通常如下

  1. 导入需要使用的gMock 名。所有gMock符号都在testing namespace中,除非它们是宏或以其他方式注明。
  2. 创建mock对象
  3. (可选)设置mock对象的默认行为
  4. 设置mock对象的expectation行为
  5. 使用断言检查mock对象执行结果
  6. mock对象销毁的时候,gMock会自动检查前面设置的expectation是否达成 一个使用示例如下
using ::testing::Return;                          // #1
 
TEST(BarTest, DoesThis) {
  // 创建mock对象
  MockFoo foo;                                    // #2
  // 设置mock对象的默认行为
  ON_CALL(foo, GetSize())                         // #3
      .WillByDefault(Return(1));
  // ... other default actions ...
  // 设置执行结果的预期
  EXPECT_CALL(foo, Describe(5))                   // #4
      .Times(3)
      .WillRepeatedly(Return("Category 5"));
  // ... other expectations ...
  // 执行一些其他的断言检测
  EXPECT_EQ(MyProductionFunction(&foo), "good");  // #5
 
 // mock对象释放,自动检测expectataion是否符合预期
}                                                 // #6