7

C++ 测试框架 GoogleTest 初学者入门篇 丙 - ENG八戒

 1 year ago
source link: https://www.cnblogs.com/englyf/p/17320950.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

C++ 测试框架 GoogleTest 初学者入门篇 丙

theme: channing-cyan

*以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg

2962155-20230415132527529-789678601.png

开发者虽然主要负责工程里的开发任务,但是每个开发完毕的功能都是需要开发者自测通过的,所以经常会听到开发者提起单元测试的话题。那么今天我就带大伙一起来看看大名鼎鼎的谷歌 C++ 测试框架 GoogleTest。

本文上接《C++ 测试框架 GoogleTest 初学者入门篇 乙》,欢迎关注公众号【ENG八戒】查看更多精彩内容。


什么是断言?断言是用来对表达式执行比较的代码块,调用时类似函数。当表达式一致时,断言返回成功,否则失败。

googletest 的断言是一组宏定义。分为 ASSERT_* 和 EXPECT_* 两种。

ASSERT_EQ(1, 2);

EXPECT_EQ(1, 2);

上面用到的两个断言都是比较输入的数据是否相等。主要区别是,ASSERT_* 在失败时终止程序运行,EXPECT_* 在失败时不会终止程序运行,但是都会返回错误信息。因而测试使用 EXPECT_* 可以发现更多的问题而不会打断测试流程。

那么 ASSERT_* 断言失败时,跟在其后的语句会被忽略执行,如果其中包含对资源的释放,那么就有会出现资源泄漏的问题,断言失败报错信息会附带有堆检查错误。这时出现的资源泄漏问题,真的有必要修复码?看具体情况而定。

另外,googletest 在断言失败后除了可以返回标准错误信息,还可以附带返回自定义错误信息,使用操作符 << 添加自定义错误信息。

ASSERT_EQ(1, 2) << "1 is not equal to 2";

EXPECT_EQ(1, 2) << "1 is not equal to 2";

任何可以传递给 ostream 的数据都可以作为自定义错误信息传递给断言,比如 C 字符串、string对象。

那么,测试的基本手段就是利用断言,除了判断型的断言之外,googletest 还提供了其它类型的断言用于协助测试,比如显式成功或失败、布尔类型断言、字符串比较断言等,详情可以前往官网查看手册。

https://google.github.io/googletest/reference/assertions.html

前面提到在 googletest 中,测试的范围分为测试套件和单个测试。测试程序可以包含多个测试套件,一个测试套件可以包含多个测试。

简单的测试一般推荐使用 TEST 宏来定义单个测试。

一般的使用方式如下

TEST(test_suite_name, test_name) {
  // test body
}

test_suite_name 是测试套件名,test_name 是单个测试的名称,书写时都应该符合 C++ 的标识符规范,而且不能包含有下划线_。更详细的命名规范可以查看下面的链接

https://google.github.io/styleguide/cppguide.html#Function_Names

那么 TEST 宏到底代表着什么?一起来看看 TEST 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST(test_suite_name, test_name)             \
  GTEST_TEST_(test_suite_name, test_name, ::testing::Test, \
              ::testing::internal::GetTestTypeId())

#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)

这么多预定义处理,不妨尝试代入上面的一般使用方式,然后展开一下,展开如下

static_assert(sizeof("test_suite_name") > 1,
              "test_suite_name must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
			  
class test_suite_name_test_name_Test : public ::testing::Test {
  public:
  test_suite_name_test_name_Test() = default;
  ~test_suite_name_test_name_Test() override = default;
  test_suite_name_test_name_Test(const test_suite_name_test_name_Test &) = delete;
  test_suite_name_test_name_Test & operator=(
      const test_suite_name_test_name_Test &) = delete; /* NOLINT */
  test_suite_name_test_name_Test
  (test_suite_name_test_name_Test &&) noexcept = delete;
  test_suite_name_test_name_Test & operator=(
      test_suite_name_test_name_Test &&) noexcept = delete; /* NOLINT */

  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const test_suite_name_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        "test_suite_name", "test_name", nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTestTypeId(),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<test_suite_name_test_name_Test>);
		
void test_suite_name_test_name_Test::TestBody() {
  // test body
}

从展开后的代码,可以看到有一堆代码,最开始有两个断言 static_assert 用来判断输入的测试套件名和测试名长度是否大于1,所以要求 TEST 宏定义输入的测试套件名和测试名都不能为空。

然后基于 ::testing::Test 派生了一个类,类名是测试套件名和测试名串接后再在末尾加上 _Test。类内声明重写 TestBody() 方法。

TEST 宏定义后面的 {} 用于定义派生类的成员方法 TestBody() 的函数体,内部填写标准 C++ 的有效语句作为测试主体,当然也包含调用 googletest 提供的模块内容,注意这个代码块是没有返回值的。代码块执行的断言失败时,或者代码崩溃,则测试 test_name 失败,否则成功。

再来看个例子

int square(const int a)
{
  // ...
}

TEST(SquareTest, PositiveNos) { 
    ASSERT_EQ(0, square(0));
    ASSERT_EQ(36, square(6));
    ASSERT_EQ(324, square(18));
}
 
TEST(SquareTest, NegativeNos) {
    ASSERT_EQ(1, square(-1));
    ASSERT_EQ(100, square(-10));
}

上面定义了两个测试 PositiveNos 和 NegativeNos,都属于测试套件 SquareTest。

googletest 在设计时就指定通过测试套件来汇总测试结果,所以验证同一个逻辑功能的测试应该定义在同一个测试套件内。

在 googletest 里什么是测试夹具?

测试夹具这个概念是为了解决当你的同一个逻辑功能测试里,有多个测试共用测试数据或者配置的问题。

需要用到测试夹具的测试一般推荐使用 TEST_F 宏来定义单个测试。

一般的使用方式如下

TEST_F(FixtureTest, test_name) {
  // test body
}

不过,TEST_F 宏的第一个输入参数不仅仅是测试套件名称,同时也是测试夹具类名。这个测试夹具类需要自己基于类 ::testing::Test 派生实现。

class FixtureTest : public testing::Test {
protected:
void SetUp() override { ... }
void TearDown() override { ... }
// custom data
};

共用的测试数据或者配置就在这个派生类里添加即可。SetUp() 用于初始化数据和配置,TearDown() 用于卸载配置。

那么 TEST_F 宏到底代表着什么,和 TEST 宏的区别在哪?一起来看看 TEST_F 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST_F(test_fixture, test_name)        \
  GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

#define TEST_F(test_fixture, test_name) GTEST_TEST_F(test_fixture, test_name)

这么多预定义处理,手痒代入一般的使用方式然后展开一下,展开如下

static_assert(sizeof("FixtureTest") > 1,
              "FixtureTest must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
class FixtureTest_test_name_Test : public FixtureTest {
  public:
  FixtureTest_test_name_Test() = default;
  ~FixtureTest_test_name_Test() override = default;
  FixtureTest_test_name_Test(const FixtureTest_test_name_Test &) = delete;
  FixtureTest_test_name_Test & operator=(
      const FixtureTest_test_name_Test &) = delete; /* NOLINT */
  FixtureTest_test_name_Test
  (FixtureTest_test_name_Test &&) noexcept = delete;
  FixtureTest_test_name_Test & operator=(
      FixtureTest_test_name_Test &&) noexcept = delete; /* NOLINT */
  
  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const FixtureTest_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        #FixtureTest, #test_name, nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTypeId<FixtureTest>(),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<FixtureTest_test_name_Test>);
void FixtureTest_test_name_Test::TestBody() {
  // test body
}

从展开后的代码来看,TEST_F 和 TEST 实现基本类似,那么使用时要遵循的规则也是一样的,除了需要传入自定义的基于 ::testing::Test 派生类,并且测试套件名就是测试夹具类名。

举个例子,有个模板类 Queue 的逻辑功能需要测试,它实现了 FIFO 的数据队列管理。

template <typename E>  // E 是元素类型
class Queue {
 public:
  Queue();
  void Enqueue(const E& element); // 数据入队
  E* Dequeue();  // 数据出队,如果队列为空则返回 NULL
  size_t size() const;  // 队列数据长度
  ...
};

然后需要基于 ::testing::Test 派生一个测试夹具类 QueueTest

class QueueTest : public ::testing::Test {
 protected:
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }

  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

夹具类 QueueTest 内定义了三个队列数据对象。SetUp() 内对数据对象初始化,q0_ 保持为空,q1_ 入队一个数据,q2_ 入队两个数据。

为什么不实现 TearDown() 呢?TearDown() 本来的设计意图是卸载配置,不是刚好可以用来清理数据吗?是的,的确可以,不过这里有个更好的选择,就是使用类析构函数来对队列清空。这里有个建议就是,能用析构函数处理的,尽量用析构函数替代 TearDown()。因为用析构函数可以确保被调用而且调用的顺序不会乱,但不是说所有情况都建议用析构函数替代 TearDown(),这里不展开了。

接着调用 TEST_F 定义两个测试,基于测试夹具类 QueueTest,测试套件名也是 QueueTest,两个测试名分别为 IsEmptyInitially 和 DequeueWorks。

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

上面的这两个测试定义,都会创建 QueueTest 类对象,分别创建而且不共用,所以数据不会相互影响。

第一个测试 IsEmptyInitially,googletest 框架会先创建 QueueTest 类对象 obj,调用 SetUp() 初始化数据和配置,执行测试。这里只执行了一个 EXPECT_EQ 断言,EXPECT_* 类型的断言失败后会返回失败信息,不会终止测试程序,继续下一步测试。然后调用 TearDown() 清理,最后执行对象 obj 的析构函数释放资源并退出当前测试。

第二个测试 DequeueWorks,执行流程与上一个类似。其中测试内容包含有 ASSERT_* 类别的断言,这种断言在失败后除了会返回失败信息,还会终止测试程序。如果断言失败之后的测试已没有意义,那么适合使用 ASSERT_* 类别的断言。

测试调用过程

其它 C++ 测试框架在测试开始前,需要你把测试排列出来,但是 googletest 不需要这么麻烦。 在 googletest 框架中,定义好测试后,只需要在 main 部分执行如下代码即可。

int main(int argc, char **argv)
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

InitGoogleTest() 可以对程序的输入命令执行解析,基于这点可以通过命令行的方式控制测试框架的运行。

继续以上面的代码为例,大致流程如下

  1. InitGoogleTest() 初始化测试框架。
  2. RUN_ALL_TESTS() 启动测试。
  3. 查找测试套件内的测试。
  4. 保存配置标志。
  5. 创建 QueueTest 实例。
  6. 调用 QueueTest 实例的 SetUp() 初始化数据配置。
  7. 执行测试。
  8. 调用 QueueTest 实例的 TearDown() 卸载数据配置。
  9. 恢复配置标志。
  10. 重复第 3 步,直到所有测试执行完毕,

RUN_ALL_TESTS() 返回 0 表示成功,否则失败。只能在主线程里调用 RUN_ALL_TESTS()。

在一般的测试里,如果在测试运行之前不需要做一些自定义的事情,而且这些事情无法在测试夹具和测试套件的框架中表达时,main 函数这部分其实都一样,那么 googletest 就在库 gtest_main 里提供了一个很方便的入口点,也就是帮你提前写好了 main 函数,你可以省去这部分,编译的时候记得链接库 gtest_main 即可。


好了,这个系列的文章就写到这里啦。

学习可以等,时间不等人!

关注我,带你学习编程领域更多核心技能!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK