Googletest 初级教程 本文翻译自官方文档,官方原文请见Googletest Primer
为了保证阅读的质量,本文部分内容并非直译,而是加入的译者的理解的意译。若有不足之处,还请斧正。
以下为翻译正文… …
简介:为什么选择googletest?
googletest可以帮助你更好地编写C++的测试用例。
googletest是由谷歌的测试技术团队根据Google的特定要求和约束开发的测试框架。无论你是在Linux操作系统、Windows操作系统或者Mac操作系统上工作,如果你在编写C++的代码,那么googletest是可以帮助到你的,并且它支持所有类型的测试,而非仅支持单元测试。
那么,什么是好的测试,以及 googletest 是如何做到这些的呢?我们认为:
-
测试用例应该是独立的且可重复的。调试由于其它测试用例的干扰而成功或失败的测试用例是令人痛苦的。googletest 通过在不同的对象上运行每个测试用例来进行隔离。当一条测试用例失败时,googletest允许你单独运行它以快速调试。 -
测试用例之间应该被良好地组织起来并能反应测试代码的架构。googletest将具有相关性的测试用例划分到同一个测试集合(test suite)中,在同一个测试集合中的测试用例,可以共享数据和子程序。这种通用的模式将会易于上手并会使测试代码变得易于维护。这样的一致性在人们切换项目并开始工作在一个新的代码库上时尤其有用。 -
测试用例应该是可移植的且可复用的。Google具有大量的平台无关的代码,所以它的测试也应该是平台无关的(译者注:这句话的含义应该是说,google内部的项目代码,有好多就是与平台无关的,那么对这些代码的测试,也应该是与平台无关的。那么如何做到对这些代码的测试是与平台无关的呢?言外之意就是,用googletest就可以了,googletest是能够做到与平台无关的,即跨平台)。googletest 适用于不同的操作系统,这些操作系统可能会有不同的编译器,支持异常或不支持异常,所以googletest测试可以使用多种不同的配置。 -
当测试用例失败时,测试框架应该尽可能多的提供有关该错误的信息。googletest不会在遇到第一个测试失败时就在此停止运行,而是只会停止当前的测试用例并执行下一个。你也可以设置这个测试用例报告非致命失败(non-fatal failure),这样就可以继续运行这个测试用例,这样你就可以在一个 运行测试-修改代码-编译并重新运行 的循环中探测并修复多个bug。译者注:测试用例失败的意思就是,测试用例报告致命失败(fatal failure)。没报fatal failure就可以继续执行,后边关于断言的介绍会让你了解什么是fatal,什么是non-fatal。在一个测试用例中报了非致命失败并让该测试用例继续执行,好处就是可以发现后续的bug(当然,后续的bug有可能和前边的bug是同一个),可以收集到该bug的更多信息便于debug;如果一次发现了多个bug,可以统一修改,然后再测试,若是遇到错误就停止运行,那就只能发现一个bug了 -
测试框架应使测试编写者摆脱内部管理的繁琐工作,并让他们专注于测试内容。googletest自动跟踪定义的所有测试,并且不需要用户枚举即可运行它们。 -
测试的运行应该是快速高效。使用googletest,你可以在测试用例之间重用共享资源,并且只需花费一次 set-up/tear-down 的开销,而无需使测试间相互依赖。译者注:set-up,即构建部署那些公共资源或者需要在测试开始前构建的环境;tear-down,即释放那些公共资源或者恢复set-up构建的环境,即set-up的逆操作
由于googletest基于广泛使用的xUnit架构,因此如果你以前使用过JUnit或PyUnit,就会对googletest感到熟悉。如果不熟悉的话,你将需要大约10分钟的时间来学习基础知识并开始使用。所以开始学习吧!
术语辨析
Note: 术语Test,Test Case和Test Suite的不同定义可能会引起混淆,因此请注意不要误解这些术语。
googletest早期使用 test case 一词来表示划分为一组的相关测试,而当前的出版物(包括国际软件测试资格委员会(ISTQB)资料和各种有关软件质量的教科书)都使用了 test suite 这一术语 。
Googletest中使用的术语 Test 与ISTQB及其它机构、教科书等的术语 Test Case 相对应 。
译者注:此章节的超链接的访问可能需要“科学上网”。
术语 Test 通常具有足够广泛的含义,包括ISTQB定义的 Test Case ,因此在这方面来看,术语 Test 不会给人带来太多困惑。但是在Google Test中使用的术语 Test Case 的意义是矛盾的,令人困惑的。
googletest最近开始用术语 Test suite 代替 Test Case 。目前首选的API是TestSuit。旧的TestCaseAPI缓慢弃用并重构。
因此,请注意这些术语的不同定义:
基本概念
当你使用googletest的时候,你要编写断言 ,断言就是那些用来检查一个条件是否为true的语句。一个断言的结果可以是成功(success),非致命失败(nonfatal failure)或者致命失败(fatal failure)。当一个致命失败发生时,它将终止对应的测试用例的执行;不是致命失败,则程序会继续执行下去。
测试用例使用断言去验证待测试代码的行为。如果一个测试用例崩溃或者产生了失败(failure),那么这个测试用例的测试结果就是失败的(即该条测试用例是不通过的);如果没有产生failure,那么该条测试用例就是成功的(即通过的)。
一个测试集合(test suite)包含一个或多个测试用例。你应该将测试用例(test case)划分到测试集合(test suite)中以反映这个测试代码的架构。当同一个测试集合中的多个测试用例需要共享一些公共的对象或者子程序时,你可以把这些公共资源放在test fixture类中。(译者注:test fixture类在后文有介绍)
一个测试程序可以包含多个测试集合。
接下来我们将讲解如何编写一个测试程序,该程序将从声明断言开始,直到成功构建测试用例和测试集合。
断言
googletest断言是一种类似函数调用的宏。你将通过断言一个函数或类的行为来测试这个函数或类。当一个断言失败时,googletest打印这个断言的源文件、行号及失败信息。你也可以添加自定义的失败信息,追加在googletest失败信息的后边。
对于一个测试点,我们有两类断言可以对它进行测试。但它们对当前的测试用例有不同的影响。ASSERT_* 这种断言在失败时会产生一个致命失败,并且终止当前测试用例。EXPECT_* 这种断言在失败时会生成非致命失败,这将不会终止当前测试用例。通常优先使用EXPECT_* 这种断言,因为这种断言可以在一个测试用例中报出更多的测试失败(译者注:这种断言出错后不会终止当前测试用例,而是继续执行。若后续的断言语句依然有不通过的,则后续的断言依然可以报出测试失败信息)。但是,如果在断言失败后继续执行程序没有任何意义的情况下,则应该使用ASSERT_* 断言。
由于ASSERT_* 断言在失败会立即返回,所以可能会跳过后边代码中清理(clean-up)代码,这可能会导致内存泄露。根据具体的情况来看,有的泄露可能不需要去刻意修复,有的可能需要去规避这种情况的产生——所以要记住这一点,如果你在断言失败后还发生了堆栈检查相关的错误可能是由于ASSERT_* 失败后没能执行clean-up代码。
要提供自定义失败消息,只需使用<< 运算符或多个该运算符序列将自定义的消息流式传输到宏中即可 。如:
ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";
for (int i = 0; i < x.size(); ++i) {
EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
任何可以流式传入ostream 的都可以流式传入断言宏。尤其是C字符串和string 对象。如果一个宽字符串(wchar_t* ,TCHAR* 在Windowss上的UNICODE 模式,或模式std::wstring )流式传输到一个断言,在打印时将被转换为UTF-8。
基本断言
基本断言做最基本的真/假条件测试。
致命(Fatal)断言 | 非致命(Nonfatal)断言 | 断言通过的条件 |
---|
ASSERT_TRUE(condition); | EXPECT_TRUE(condition) | condition 为真 | ASSERT_FALSE(condition); | EXPECT_FALSE(condition) | condition 为假 |
请记住,当它们断言失败时,ASSERT_* 断言会产生一个致命失败(fatal failure)并从当前的测试用例中返回,而EXPECT_* 断言产生一个非致命失败(nonfatal failure),测试用例继续执行。无论那种断言,只要是断言失败了,就意味着包含这条断言的测试用例测试失败。
可用性:Linux,Windows,Mac。
二元比较断言
本章描述了那些比较两个值的断言。
致命(Fatal)断言 | 非致命(Nonfatal)断言 | 断言通过的条件 |
---|
ASSERT_EQ(val1, val2); | EXPECT_EQ(val1, val2); | val1 == val2 | ASSERT_NE(val1, val2); | EXPECT_NE(val1, val2); | val1 != val2 | ASSERT_LT(val1, val2); | EXPECT_LT(val1, val2); | val1 < val2 | ASSERT_LE(val1, val2); | EXPECT_LE(val1, val2); | val1 <= val2 | ASSERT_GT(val1, val2); | EXPECT_GT(val1, val2); | val1 > val2 | ASSERT_GE(val1, val2); | EXPECT_GE(val1, val2); | val1 >= val2 |
这些val参数的类型必须是可以被断言的比较运算符进行比较的,否则会出现编译器错误。我们之前需要这些val参数类型支持<< 运算符以流式传输到ostream ,但这不再是必需的了。如果<< 是被val类型支持的,当断言失败时<< 将被调用去打印这些参数;否则googletest会尝试以最佳方式打印它们。更多关于“如何自定义打印参数”的信息,请点击此文档.
这些断言可以使用用户定义的类型,但前提是你定义了相应的比较运算符(例如== 或< )。由于Google C ++ style Guide 不建议这样做,因此您可能需要使用ASSERT_TRUE() 或EXPECT_TRUE() 断言两个用户自定义类型的对象的相等性。
但是,在可能的情况下,ASSERT_EQ(actual, expected) 优于ASSERT_TRUE(actual == expected) ,因为前者在断言失败时会打印actual和expected的值。
断言宏的每个参数都会被“取走(evaluate)”一次,因此有一些副作用是在所难免的。就像C/C++函数一样,参数的取走的顺序是没有明确规定的(即,编译器可以自行选择取走参数的顺序)。所以你的代码逻辑不能依赖于任何特定的参数取走(argument’s evaluation)顺序。译者注:函数的参数,是先取走第一个参数,还是先取走第二个参数,抑或是其他顺序,这是由编译器决定的。可能这样直接说比较晦涩,可以参看本文最后的例子 argument evaluation例子
ASSERT_EQ() 基于指针本身的值来判断指针是否相等。如果传入的是C字符串(即 char *),ASSERT_EQ() 比较的是这两个字符串是否在同一块内存上(即两个指针的地址值是否相等),而非比较这两个字符串是否相等。因此,如果你想比较C字符串的值,应使用ASSERT_STREQ() ,这将会在后文提到。需要特殊说明的是,判断一个C字符串是否是NULL ,应使用ASSERT_STREQ(c_string, NULL) 。当支持c++11的时候,也可以使用ASSERT_EQ(c_string, nullptr) 。如果是比较两个c++string 对象,应使用ASSERT_EQ 。
在进行指针比较时,请使用*_EQ(ptr, nullptr) 和*_NE(ptr, nullptr) 代替*_EQ(ptr, NULL) 和*_NE(ptr, NULL) 。有关更多详细信息,请参见FAQ。
如果你的代码涉及到浮点数,你可能想要使用类似这些宏的浮点类型的宏进行断言,以避免四舍五入带来的问题。详情见Advanced googletest Topics。
本章节所涉及到的宏支持传入窄字符串对象和宽字符串对象(string 和wstring )。
可用性:Linux,Windows,Mac。
历史变更注意:在2016年2月之前,*_EQ 有这样一个使用惯例:ASSERT_EQ(预期值,实际值) ,所以现存的很多代码都是按照这样的参数顺序写的。现如今,*_EQ 以同样的方式处理两个参数。译者注:即按ASSERT_EQ(预期值,实际值)传入参数也行,按ASSERT_EQ(实际值,预期值)传入参数也行。
字符串比较断言
这类断言比较两个C字符串。如果你想比较两个string 对象,使用EXPECT_EQ ,EXPECT_NE 等宏即可。
致命(Fatal)断言 | 非致命(Nonfatal)断言 | 断言通过的条件 |
---|
ASSERT_STREQ(str1,str2); | EXPECT_STREQ(str1,str2); | 字符串内容相同 | ASSERT_STRNE(str1,str2); | EXPECT_STRNE(str1,str2); | 字符串内容不同 | ASSERT_STRCASEEQ(str1,str2); | EXPECT_STRCASEEQ(str1,str2); | 字符串内容相同(忽略大小写) | ASSERT_STRCASENE(str1,str2); | EXPECT_STRCASENE(str1,str2); | 字符串内容不同(忽略大小写) |
注意,断言中的"CASE"表示忽略大小写。NULL 空指针和空字符串被认为是不同的。
*STREQ* 和*STRNE* 也能传入宽C字符串(wchar_t* )。如果比较两个宽字符串的断言失败,字符串将会以UTF-8窄字符串的形式打印。
可用性:Linux,Windows,Mac。
另请参阅:更多关于字符串比较的技巧(如子串,前缀,后缀和正则匹配等),请见这篇Advaced googletest Guide。
简单测试
以下步骤创建一个测试用例:
- 使用宏
TEST() 定义一个测试用例。这就是一个普通的没有返回值的C++函数。 - 在
TEST() 的函数体中,可以包含任意你想添加的合法有效的C++语句,并使用googletest的断言去检验。 - 测试的结果是由断言决定的。该测试用例中的任一断言失败(无论是致命断言还是非致命断言),或者测试用例崩溃(译者注:比如你写的这个测试用例发生了段错误),该测试用例就被认定为失败的。否侧,断言成功。
TEST(TestSuiteName, TestName) {
... test body ...
}
TEST() 的参数,依次由抽象到具体(译者注:由整体到局部)。第一个参数是测试集的名称,第二个参数是测试用例的名称,该测试用例属于第一个参数所指定的测试集。这两个名称都必须是合法的C++标识符,并且不应该包含下划线(_ )。一个测试用例的完整名称由包含它的测试集和它自己的名称组成。不同测试集里的测试用例可以使用同样的名称。
如,编写一个简单的整型函数:
int Factorial(int n);
测试这个函数的测试集可能是这样的:
TEST(FactorialTest, HandlesZeroInput) {
EXPECT_EQ(Factorial(0), 1);
}
TEST(FactorialTest, HandlesPositiveInput) {
EXPECT_EQ(Factorial(1), 1);
EXPECT_EQ(Factorial(2), 2);
EXPECT_EQ(Factorial(3), 6);
EXPECT_EQ(Factorial(8), 40320);
}
googletest通过测试集来组织这些测试的结果,所以逻辑相关的测试用例应该被放在相同的测试集中。换言之,逻辑相关的测试用例,TEST() 的第一个参数应该是一样的。在上边的例子中,有两个测试用例,HandlesZeroInput 和HandlesPositiveInput ,它们属于同一个测试集FactorialTest 。
在命名你的测试集和测试用例时,应该遵循和命名函数和类相同的规范。
可用性:Linux,Windows,Mac。
Test Fixtures:使用同一份公共资源给多个测试用例
如果你发现你写了多个测试用例,它们都要用到相似的数据,你可以使用test fixture。这将允许你给不同的测试用例重用一些相同的配置。
以下步骤创建一个fixture:
- 从
::testing::Test 派生一个类。类体以protected: 开始,因为我们想从子类中访问fixture的成员。 - 在这个类中,声明一些你预计要使用的对象。
- 如果需要的话,编写一个默认的构造函数或
SetUp() 函数来为每个测试用例初始化fixture中的对象。一个经常出现的错误是将SetUp() 拼写为Setup() ——在C++11中可以使用重写 保证你的拼写正确。 - 如果需要的话,编写一个析构函数或者
TearDown() 函数去释放你在SetUp() 中分配的资源。关于何时应该使用构造/析构函数和何时应该使用SetUp()/TearDown() ,参见FAQ。 - 如果需要的话,定义一些需要共享的子程序。
当时用fixture时,使用TEST_F() 替代TEST() ,因为TEST_F() 可以使你访问到fixture中的对象和子程序。
TEST_F(TestFixtureName, TestName) {
... test body ...
}
同TEST() 一样,TEST_F() 的第一个参数是测试集名称,但TEST_F() 要求第一个参数必须是fixture类的类名。你可能已经猜到,_F 是给fixture用的。
不幸的是,C++的宏规则不允许我们(译者注:这里的“我们”指googletest维护者开发者,本段内容是在解释为什么要搞一个TEST_F()出来,而不是统一使用TEST())仅创建一个宏就可以操纵不同的测试,使用错误的宏将会导致编译错误。
同样的,你必须在在TEST_F() 中使用fixture之前定义好fixture类,否则你将看到编译错误virtual outside class declaration 。
googletest会在运行的时候,为每一个使用TEST_F() 定义的测试用例创建一个新的fixture对象,立即调用SetUp() 进行初始化,然后运行测试用例,之后会通过调用TearDown() 来清理资源,最后删除fixture对象。需要注意的是,同一个测试集中不同的测试用例使用的是不同的fixture对象,googletest总是在为下一个测试用例创建fixture之前,删除当前测试使用后的fixture。googletest不会给多个测试用例重用同一个fixture对象。一个测试用例对fixture的任何操作不会影响到其他测试用例。
让我们来编写一个使用fixture测试FIFO队列的例子,队列的接口如下:
template <typename E>
class Queue {
public:
Queue();
void Enqueue(const E& element);
E* Dequeue();
size_t size() const;
...
};
首先,定义一个fixture类。一般的,你应该给一个待测类名为Foo 的类创建一个名为FooTest 的fixture类。
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
在这个例子中,不需要 TearDown(),因为除了析构函数已经完成的操作之外,我们不必在每次测试后再进行其他额外的清理。
接下来我们使用TEST_F() 和fixture编写测试用例。
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;
}
以上代码使用了ASSERT_* 和EXPECT_* 断言。按照经验来讲,当你想在断言失败后继续运行这个测试用例并且想暴露出后续可能存在的更多的错误的情况下,应该使用EXPECT_* ;当在断言失败后继续运行测试毫无意义的情况下,应该使用ASSERT_* 。如,在Dequeue 测试用例中,第二个断言是ASSERT_NE(nullptr, n) ,因为我们在后边的代码中需要对指针n 解引用,这将会在n 是NULL 的时候导致段错误。
当这些测试用例在运行时:
- googletest构造一个
QueueTest 对象(如我们可以叫它t1 )。 t1.SetUp() 初始化t1 。- 第一个测试用例(
IsEmptyInitially )基于t1 运行。 t1.TearDown() 在这个测试用例结束后被调用。t1 被析构。- 上述步骤在一个新的
QueueTest 对象上重复,这次基于新的QueueTest 对象(可以认为这个对象是t2 )运行第二个测试用例DequeueWorks 。
可用性:Linux,Windows,Mac。
调用测试用例
TEST() 和TEST_F() 隐式地把它们的测试注册给 googletest,所以,不像许多其他C++测试框架,你不必为了运行它们而一一列举它们。
在你定义完测试用例之后,你可以使用RUN_ALL_TESTS() 运行它们,当所有测试成功时返回0 ,否则返回1 。注意RUN_ALL_TESTS() 运行你链接的所有测试——它们可以来自不同的测试集,甚至是不同的源文件。
当RUN_ALL_TESTS() 被调用时:
- 保存所有googletest flags的状态。
- 为第一个测试用例创建fixture对象
- 通过
SetUp() 初始化fixture对象。 - 基于这个fixture对象运行第一个测试用例。
- 通过调用
TearDown 清理fixture对象。 - 删除fixture。
- 恢复所有googletest flags的状态。
- 重复以上所有步骤,知道所有测试运行完毕。
如果一个致命失败发生了,下面的步骤将被跳过。
重要提示:切记不要忽略RUN_ALL_TESTS() 的返回值,否则你将会得到编译错误。发生这种情况是因为自动化测试服务是根据退出码(exit code)来判定测试是否通过的,而不是标准输入输出,因此你的main() 函数必须返回RUN_ALL_TESTS() 的值。 ? 另外,你应该只调用一次RUN_ALL_TESTS() 。多次调用会和googletest的一些高级特性(如线程安全death_tests)发生冲突导致高级特性不被支持。
可用性:Linux,Windows,Mac。
编写main()函数
大多数用户不需要编写main 函数,只要链接gtest_main 库就可以,该库提供了合适的入口点。当你想在测试用例执行之前执行一些自定义的操作,但这些操作又不能通过现有的框架去添加(如fixture),就需要编写main 函数了。
你编写的main 函数需要返回RUN_ALL_TESTS() 的返回值。
可以参考以下模板:
#include "this/package/foo.h"
#include "gtest/gtest.h"
namespace my {
namespace project {
namespace {
class FooTest : public ::testing::Test {
protected:
FooTest() {
}
~FooTest() override {
}
void SetUp() override {
}
void TearDown() override {
}
};
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);
}
TEST_F(FooTest, DoesXyz) {
}
}
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
::testing::InitGoogleTest() 函数解析 googletest 标记的命令行参数,并移除所有已识别的标记。这允许用户通过不同的标记控制测试程序的行为,我们将在AdvancedGuide(高级教程)中描述相关的内容。你必须在调用RUN_ALL_TESTS() 之前调用这个函数,否则标记将无法得到适当的初始化。
在Windows下,InitGoogleTest() 同样也可以基于宽字符串使用,因此它也可以被用于以UNICODE 模式编译的程序。
你可能觉得编写一个这样的main 函数太麻烦了,我们觉得也是,这就是Google Test 提供了一个基础的main 函数实现的原因。如果它能够满足你的需求的话,仅需要将你的测试用例和库gtest_main 链接就可以了。
注:ParseGUnitFlags() 已弃用,推荐使用InitGoogleTest() 。
已知限制
- Google Test被设计为线程安全的。在
pthread 库可用的系统上是线程安全的。目前,在其他系统(例如Windows)上从两个线程并发使用Google Test断言是不安全的。在大多数测试中,这不是一个问题,因为断言通常是在主线程中完成的。如果您想提供帮助,可以自愿在gtest-port.h中为您的平台实现必要的同步原语。
以下内容为译者添加
argument evaluation例子
在C/C++中,argument evaluation的顺序是不固定的。下边的代码打印x = 1, y = 2 或x = 2, y = 1 ,这取决于编译器。到底该打印哪个,C/C++的标准是没有规定的,也就是说,你用a编译器可能输出第一种情况,用b编译器可能输出第二种情况,甚至用同一个编译器,添加不同的优化选项、编译选项等都可能会导致打印的结果不一致。
int f(int x, int y) {
printf("x = %d, y = %d\n", x, y);
}
int get_val() {
static int a = 0;
return ++a;
}
int main() {
f(get_val(), get_val());
}
在googletest中,不能出现以下情况:
ASSERT_LT(get_val(), get_val());
在这种情况下,如果ASSERT_LT()中的左侧的get_val()先执行,则断言通过;若是右侧的get_val()先执行,则断言失败。即你的断言中,你的代码中,不应该出现这种由于断言参数的执行顺序不同导致断言结果不同的现象。
|