主要参考资料
库介绍
下面是本菜鸟为了理解框架而做的一点笔记。如果您已经初步了解过googletest
,建议跳过此节。
googletest
是一个由Google开发的、可在多平台下使用的C++测试框架,允许开发者通过相对优雅的方式组织和运行测试源码,得到稳定可信的测试结果。
与ISTQB等对测试相关概念的定义不同,Google官方在说明文档中将测试(test) 描述为一个检查测试程序内特定值是否符合预期的逻辑单元。而对应的测试用例(test case) 描述一组高关联性测试的组合,测试集(test suite) 则是一组测试目的一致的测试用例的集合。例如:当前需要对一个红黑树类lewis::rbtree<T, Allocator>
进行测试,那么所有测试该类的测试用例可称为一个测试集,所有测试同一个函数/特性的测试,例如所有测试删除元素函数remove(const T&)
的测试,可称为一个测试用例。
根据Google官方的说法,一个好的测试应当满足如下特点:
- 独立且可重复。测试应当不依赖于其他测试而独立运行,方便测试者进行调试。
- 高度组织,可以反映被测试代码的结构。相同或相近目的的测试源码应当组织在同一个测试集或测试用例下。
- 复用性和平台无关性强。否则移植又是一大痛苦。
- 信息丰富。如果测试失败,测试程序应当提供尽可能多的有关测试失败的细节。
- 应当迎合测试者,让测试者的精力集中在测试程序上,而不是让测试者用繁文缛节去迎合测试框架本身。
- 快速。否则每次跑测试都要先让测试大哥冲杯咖啡吗。
安装和部署
googletest
可在多数主流平台下安装和部署,但平台下必须已经安装:
CMake
、bazel
两种构建工具之至少一种- 至少一套被对应构建工具支持的生成工具链,如
MinGW
、Visual 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。