C/C++:测试框架入门之googletest

主要参考资料

Googletest Primer

大佬A(参考了TEST()用法)

大佬B(参考了断言列表)

大佬C(参考了一些细节内容)

库介绍

下面是本菜鸟为了理解框架而做的一点笔记。如果您已经初步了解过googletest,建议跳过此节。

googletest是一个由Google开发的、可在多平台下使用的C++测试框架,允许开发者通过相对优雅的方式组织和运行测试源码,得到稳定可信的测试结果。

与ISTQB等对测试相关概念的定义不同,Google官方在说明文档中将测试(test) 描述为一个检查测试程序内特定值是否符合预期的逻辑单元。而对应的测试用例(test case) 描述一组高关联性测试的组合,测试集(test suite) 则是一组测试目的一致的测试用例的集合。例如:当前需要对一个红黑树类lewis::rbtree<T, Allocator>进行测试,那么所有测试该类的测试用例可称为一个测试集,所有测试同一个函数/特性的测试,例如所有测试删除元素函数remove(const T&)的测试,可称为一个测试用例。

根据Google官方的说法,一个好的测试应当满足如下特点:

  • 独立且可重复。测试应当不依赖于其他测试而独立运行,方便测试者进行调试。
  • 高度组织,可以反映被测试代码的结构。相同或相近目的的测试源码应当组织在同一个测试集或测试用例下。
  • 复用性和平台无关性强。否则移植又是一大痛苦。
  • 信息丰富。如果测试失败,测试程序应当提供尽可能多的有关测试失败的细节。
  • 应当迎合测试者,让测试者的精力集中在测试程序上,而不是让测试者用繁文缛节去迎合测试框架本身。
  • 快速。否则每次跑测试都要先让测试大哥冲杯咖啡吗。

安装和部署

googletest可在多数主流平台下安装和部署,但平台下必须已经安装:

  • CMakebazel两种构建工具之至少一种
  • 至少一套被对应构建工具支持的生成工具链,如MinGWVisual Studio

如果你在Windows下,更建议使用vcpkg等第三方库管理器安装部署googletest,如果自行部署,则要额外注意,使用测试库的平台必须和构建该库的平台对应;如果你在Linux下,则可以考虑自行安装部署或者通过包管理器安装部署该库。无论在何种平台下,如果你选择自行安装部署,必须下载一份该库的源码包:google/googletest,再在源码包根目录下执行构建命令。

另外,为了使用googletest,你也必须在使用该库的项目中手动添加引入该库的声明:

# CMake
find_package(GTest CONFIG REQUIRED)
target_link_libraries(main PRIVATE 
  GTest::gmock 
  GTest::gtest 
  GTest::gmock_main 
  GTest::gtest_main
)

内置测试机制

googletest引入了一系列可用于测试的功能,它们可以相对容易地在源码中使用。

断言

与<cassert>中的assert(cond)宏类似但不完全相同,googletest的断言是一系列根据测试条件是否满足而做出不同反应的宏。这类宏的统一格式为<ACTION>_<CONDITION>,其中ACTION可以为传统断言(ASSERT)或者预期(EXPECT),区别在于ASSERT测试失败将会中止测试用例,EXPECT只会提示测试失败,一般不会干扰测试流程。而CONDITION包括但不限于下列情形之一,关于这些情形对应的断言内容细节,敬请阅读我在文首列出的参考博客

用途CONDITION格式
比较值EQ、NE、GT、GE、LT、LE(val1, val2)
判断真假TRUE、FALSE(expr)
字符串比对(大小写敏感)STREQ、STRNE(cstr1, cstr2)
字符串比对(忽略大小写)STRCASEEQ、STRCASENE(cstr1, cstr2)
浮点数比对FLOAT_EQ、DOUBLE_EQ(float1, float2)
浮点数近似NEAR(float1, float2, eps)
类型异常检查THROW(stmt, except_type)
任意异常检查ANY_THROW、NO_THROW(stmt)
谓词断言(无格式)PREDk(1<=k<=5)(pred, val1, …, valk)
谓词断言(带格式)PRED_FORMATk(1<=k<=5)(pred, val1, …, valk, fmt)

断言是一种基本测试单元,一般一个测试用例会包含若干个断言。下面是一个使用示例:

int Factorial(int x) {
  if (x <= 0) return 1;
  return x * Factorial(x - 1);
}

TEST(FactorialTest, RegularTest) {
  int _fact = 1;
  for (int i = 1; i <= 12; ++i) {
    EXPECT_EQ(Factorial(i), (_fact *= i));
  }
}

TEST(FactorialTest, OverflowTest) {
  // these lines are duplicated intentionally.
  EXPECT_EQ(Factorial(17), 355687428096000ll);
  ASSERT_EQ(Factorial(17), 355687428096000ll); // ASSERT :)
  EXPECT_EQ(Factorial(17), 355687428096000ll);
  EXPECT_EQ(Factorial(17), 355687428096000ll);
}

为了丰富断言测试的输出内容,我们可以利用<<运算符追加输出信息:

EXPECT_EQ(Factorial(i), (_fact *= i)) << ' ' << "Factorial is now " << _fact;

结果如下:

在这里插入图片描述

从结果可以看到RegularTest通过了,但是OverflowTest因为计算过程中的乘法溢出,没有通过。另一个耐人寻味的地方在于,OverflowTest的四处断言都不能成立,但只报告了前两处,这是因为第二处的断言为ASSERT_*型,在断言失败后中止了该测试。

死亡测试ASSERT_DEATH(stmt, matcher))是一类比较特殊的断言测试,它测试特定的语句在运行时是否以预期的形式崩溃。如果语句没有崩溃,或者崩溃了但错误信息无法和matcher匹配,则测试失败。下面是一个成功的死亡测试示例,注意DeathTest结尾的测试集在测试中有更高的优先级。

TEST(DeathTest, DemoTest) {
  void* (*func)() = nullptr;
  EXPECT_DEATH(func(), "");
}

死亡测试实际上是一种特殊形式的退出测试(ASSERT_EXIT)。有关死亡测试和退出测试的更多内容,敬请阅读这篇文章,这里不再赘述。

人为干预测试

一般人为干预是指通过在测试用例中添加SUCCEED()FAIL()ADD_FAILURE()来人为改变测试结果的手段。

注意:只有FAIL()会中止一个测试,其余两者都不会实质性改变测试过程,而只是标记一个额外的成功或失败测试。例如,在已经失败过的断言后添加SUCCEED()不会改变测试用例的结果,也不会中止测试用例;但在未失败的测试中添加ADD_FAILURE(),会使测试变为失败状态。

静态类型检查

静态类型检查通过模板机制检查两个类型,如果不匹配则无法通过编译。

template <class Alloc, class T> 
class IsAlloc {
public:
  void Check() { 
    ::testing::StaticAssertTypeEq<Alloc, std::allocator<T>>(); 
  }
};

template <class T>
class FakeAllocator {};

TEST(TypeAssertionTest, Demo) {
  IsAlloc<FakeAllocator<int>, int>().Check();
}

测试用例编写

前面已经演示了测试用例的写法,这里说明一下:

TEST(GoodTestSuite, GoodTestCase) {
  // do some retarded tests >:)
}

TEST(suite, case)表示一个测试用例,suite为测试集名称,case为测试用例名称,后面的大括号内是一个测试函数。同一个测试集的所有测试用例相邻执行。

测试程序的入口函数应当添加初始化googletest的有关声明。要运行程序中的所有测试用例,应添加RUN_ALL_TESTS()

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

测试固件(test fixture)

不可否认,上面提到的测试用例和断言机制都很简练。但是,如果各个测试用例之间需要共享一些数据,或者测试用例之间的重复度很高(例如:编译器前端的表达式测试),这个时候编写一堆TEST()显然是不符合测试实际的。

Google提供了“测试固件”(Primer中称其为test fixture,但博客资料大多不提供相关内容,这里提供一个十分蹩脚的译名),用于解决测试用例重复度高、相互依赖性强的问题。为了利用该机制,你需要:

  • 编写一个::testing::Test的派生类,并以protected:开头。因为测试用例的本质是一个测试集的派生,这样可以让所有的测试用例都使用该类定义的内容。
  • 在类内声明所有计划在各个测试用例间共享、但不对外公开的成员变量或函数。
  • 如果需要,重载SetUp()成员函数和TearDown()成员函数,描述类内成员是如何初始化和销毁的。

下面是一个test fixture的示例:

class StdVectorTestFixture : public ::testing::Test {
 protected:
  virtual void SetUp() override {
    rnd.seed(time(nullptr));
    int cnt = 50;
    while (cnt--) shared_.emplace_back(rnd());
  };

  virtual void TearDown() override{};

  static std::mt19937 rnd;
  static std::vector<int> shared_;
};

注意:对使用同一个fixture的各个测试用例,如果需要在这些用例中间共享数据成员,请将其声明为static变量。因为用例本质上是创建一个派生实例去执行测试,测试完毕后,实例即被销毁,所以非static数据成员不能达到共享数据的目的。Google官方不提倡对这些测试用例共享数据,但如果确需共享,也只能这么做。

test fixture的复用是通过TEST_F(fixture, test)方式实现的,fixture指代fixture的类名,test指代测试用例名。通过TEST_F创建的测试用例可以使用fixture类定义的非private成员。

TEST_F(StdVectorTestFixture, SortingTest) {
  std::sort(shared_.begin(), shared_.end());
  for (size_t idx = 0; idx < shared_.size() - 1; ++idx)
    EXPECT_TRUE(shared_[idx] <= shared_[idx + 1]);
}

测试事件

基于googletest的测试程序本质上是一堆测试事件的集合。测试用例的有序执行和测试有关环境的准备、清理,都离不开测试事件。根据事件执行的位置不同,可以将测试事件分为三种:

  • TestCase级事件:该事件与TestCase同级,会在TestCase的前后执行。
  • TestSuite级事件:该事件会在一个TestSuite开始执行之前或执行完毕后执行。
  • 全局级事件:该级别事件会比所有TestSuite更早或更晚执行。

前两类事件可以借助fixture机制实现。对于TestCase级事件,如果通过复用fixture编写用例,则SetUp()一定在用例前被调用,TearDown()一定在用例后被调用;对于TestSuite级事件,fixture类中的两个静态方法SetUpTestCase()TearDownTestCase()分别会在第一个用例前和最后一个用例后执行,实现这两个函数便可实现自定义的TestSuite级事件。

class TestSuiteEventDemo : public ::testing::Test {
 protected:
  static void SetUpTestCase() {
    shared_ = ::operator new(sizeof(size_t) * 120);
  }
  static void TearDownTestCase() {
    delete[] shared_;
    shared_ = nullptr;
  }
  static void* shared_;
};

全局事件比较特殊。它不能借助fixture实现,而是需要依赖googletest中的环境(Environment)概念实现。测试程序会在开始测试之前调用所有已注册环境的SetUp()方法,会在全部测试结束后调用TearDown()方法。编写::testing::Environment的派生类,重载这两个方法,可以实现全局级事件。

class TestEnvironmentDemo : public ::testing::Environment {
 public:
  virtual void SetUp() {
    std::cout << "Customized Environment SetUp" << std::endl;
  }
  virtual void TearDown() {
    std::cout << "Customized Environment TearDown" << std::endl;
  }
};

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

参数化测试

参数化测试着重于解决某些测试用例内部的同质化问题。例如:某些测试用例会使用十数个、数十个或上百个参数,逐个编写将是十分耗时的工作。参数化是一种根据参数列表复制测试用例,从而有效降低带有大量参数的测试用例编写成本的思路,所幸Google的大神也考虑到了这个问题,在googletest中提供了对应的支持。

Google告诉我们,利用参数化功能必须经过三步:

  • 确定参数类型是什么。否则参数化也无从谈起,对吧。
  • 定义拿到参数的值之后,做些什么样的测试。这一步应当定义测试用例的样式,指定当用例取得参数时,进行怎样的测试。
  • 确定参数的范围,即我们要送哪些参数进测试用例。这一步和上一步的顺序其实无所谓。

参数化测试是由::testing::TestWithParam<T>派生的,必须定义一个对应的派生类,派生类内不需要额外添加内容,但需要通过INSTANTIATE_TEST_SUITE_P(prefix, suite, param_generator)确定使用参数的范围。在类定义的后文通过TEST_P(suite, test)创建使用该参数的测试,同一套参数可以使用于多个测试用例。

class ParamTestDemo : public ::testing::TestWithParam<int> {};

INSTANTIATE_TEST_SUITE_P(ParamList, ParamTestDemo, ::testing::Range(4, 14));

TEST_P(ParamTestDemo, Test1) {
  int n = GetParam();
  EXPECT_GE(1 << n, n * n);
}

关于INSTANTIATE_TEST_SUITE_P的解释如下:

  • prefix是前缀名,只在测试输出中有实际意义。
  • suite是测试集名,也就是我们定义的类名。因为定义的这个类本质上是一个带参的fixture,在作用上等价于一个测试集。
  • param_generator是一系列(不局限于一个)参数值生成器,包括范围生成器、枚举生成器、迭代生成器、组合生成器等。

关于参数值生成器的解释:

  • ::testing::Range(start, end, [step]):生成一组处于区间[start, end)的、步长为step(默认为1)的参数。
  • ::testing::Values(v1, v2, ..., vN):生成N个参数,参数值由宏参数指定。
  • ::testing::ValuesIn(container)::testing::ValuesIn(begin, end):生成一组处于容器内的,或者是在两个迭代器之间的参数。
  • ::testing::Bool(val):生成true和false两个值。
  • ::testing::Combine(tup1, tup2, ..., tupN):组合若干个参数值生成器,生成所有生成器参数序列的笛卡尔积。例如,Combine(Values(1, 2), Values(3, 4))对应参数序列std::tuple(1, 3), std::tuple(1, 4), std::tuple(2, 3), std::tuple(2, 4)

关于TEST_P(suite, case)的解释:总体功能和TEST_F并无二致,但suite必须是参数化测试类。Google其实做过将各类TEST宏合并的尝试,但很可惜C++的宏机制不允许这么做。

除此之外,Google也提供了参数类型不同时的解决方案。具体参考这篇文章,大佬tql!

更深层次的问题

googletest提供的功能非常繁多,上面列写的所有内容加起来也只是冰山一角。其余比较重要的内容包括但不限于:

  • 运行参数
  • 输出格式修改
  • 断言追踪
  • XML/JSON格式导出测试日志
  • 工厂模式注册测试用例

所有有关内容以Google的官方说明为准:GoogleTest User Guide

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值