Getting started with Google C++ Testing Framework
译者:Koala++ / 屈伟
Google C++测试框架可以帮助你写出更好的C++测试。
无论你是在Linux,Windows,或是Mac上工作,如果你在使用C++编码,那么Google Test都可以帮助你写测试。
什么是一个优秀的测试呢?Google C++测试框架是如何来帮助我们写出优秀的测试呢?我们相信:
1. 测试应该是独立的和可重复的。在依赖于其它测试的结果的环境下调试一个测试是很痛苦的。Google C++测试框架将每个测试都在不同的对象上独立运行,这使得测试之间是独立的。当一个测试失败时,Google C++测试框架允许你在一个独立的环境中去快速调试。
2. 测试应该有着好的组织结构并且反应被测试代码的结构。Google C++测试框架将相关的测试组织成可以共享数据和步骤的测试用例。这种常见的方式使得测试可以有着好的组织结构并使测试容易维护。在程序员开发另一项目开始测试新的测试时,这种测试的一致性特别有用。
3. 测试应该是可移值的和可重用的。开源社区有大量的平台无关的代码。Google C++测试也应该是平台无关的。Google C++测试框架可以在不同的操作系统上工作,支持多种编译器(gcc,MSVC和其它),所以Google C++测试框架可以很容易地支持多种配置。(注意:当前的版本只包含Linux的编译脚本,我们正在积极地编写支持其它平台的脚本。)
4. 当测试失败时,测试框架应该尽可能多地提供关于这个错误的信息。Google C++测试框架不会在遇到第一个失败的测试后停止。相反,它只是在这个错误的测试上停止,并且继续执行下一测试。你可以在测试上设置一个错误为非致命的,框架会在遇到这个错误后,这个测试会继续执行。这样,你就可以在一次运行-编辑-编译中检测并解决多个bug。
5. 测试框架应该将编写测试的程序员从杂事中解放出来,让他们专注于测试内容。Google C++测试框架自动记录所有定义的测试,并且不需要用户通过枚举测试来运行它们。
6. 测试应该是快速的。使用Google C++测试框架,你可以通过一次调用set-up/tear-down就可以让多个测试重用共享资源,并可以使测试之间不相互依赖。
因为Google C++测试框架是基于著名的xUnit架构,所以如果你以前使用过JUnit或是PyUnit,你会感觉用它很熟悉。如果你没有用过,它也只需要花你10分钟的时间来学习,然后你就可以开始使用了。
注意:本文中我们有时将Google Test测试框架非正式地称为Google测试。(在翻译中会使用 GTest)。
要使用GTest写测试,你需要先将GTest编译成一个库,然后将你的测试链接上这个库。我们已经为几个常见的编译系统提供了编译文件:在GTest的根目录下,有为Visual Studio提供msvc/,xcode/为Mac XCode,make/为GNU make,codegear/为Borland C++ Builder,以及autotools脚本(不建议使用)和为CMake的CMakeLists.txt(建议使用)。如果你的编译系统不在上述系统中,你可以看一下make/Makefile来学习GTest是如何编译的(简单地说你需要将GTEST_ROOT/include加入头文件查找路径,再编译GTEST_ROOT下src/gtest-all.cc,其中GTEST_ROOT是GTest的根目录)。
你编译了GTest库之后,你可以编译你的测试了。你要确认你已经将GTEST_ROOT加入到头文件查找路径中了,只有这样编译器才能在编译你的测试时找到”gtest/gtest.h”。将你的测试工程链接到GTest库(比如,在Visual Stdio中,可以在gtest.vcproj上添加一个依赖)。
如果你仍然有疑问,你可以看一下GTest自带的测试,看它们是如何编译和使用的。
你可以通过写断言来开始使用GTest,断言是检查一个条件是否为真的语句。一个断言的结果可以是成功,非致命失败,致命失败。如果一个致命失败发生了,它会中止当前的函数,否则程序会继续执行。
测试函数(Tests)用断言来验证被测试代码的行为。如果一个测试崩溃或是有一个失败的断言,那么这个测试函数就失败了,否则测试函数是成功的。
一个测试用例(Test case)可以包含一个或多个测试函数。你可以将你的测试函数组合到一个测试用例中,这样这些测试函数就可以共享对象和子程序。
一个测试程序(Test program)可以包含多个测试用例。
我们接下来会解释如何写一个测试程序,我们从单个断言开始学起,后来再学习测试语句和测试用例。
GTest断言(s)是由一些看起来像是函数的宏组成的。测试一个类或是函数的方法是通过对它们的行为进行断言。如果断言失败,GTest会打印断言所在的文件名和行号,还有失败信息。你还可以在失败信息之后附加自定义的失败信息。
断言(s)的名字通常是成对的,一对断言都是测试相应的内容,但对被测试函数有着不同的作用(比如ASSERT_TURE和ASSERT_FALSE)。ASSERT_*断言在失败时产生致命失败,这将会中止当前的函数执行。通常EXPECT_*更好一些,因为它们允许一个测试函数中报告多个失败。但是,如果某些错误是非常严重的错误,一旦发生,没有必要再向下执行时,用ASSERT_*则是合理的。
因为一个失败的ASSERT_*会立即从当前函数返回,它可能会跳过后面的清理代码,而这可能产生资源泄露。根据泄露的类型,这种泄露可能是需要或不需要修复的。所以你在发现堆内存泄露时不要忘记这可能是断言错误引起的。
要提供一个自定义的失败信息,只需要用<<操作符流式输出,比如:
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对象也是可以的。如果是一个宽字符串(Windows中的wchar_t*,TCHAR*,或是std::wstring)流式输出到断言,它会在打印时被转换成UTF-8编码。
下面的断言用于基本的true/false条件测试。
致命断言 |
非致命断言 |
验证 |
ASSERT_TRUE(condition) |
EXPECT_TRUE(condition) |
condition为真 |
ASSERT_FALSE(condition) |
EXPECT_FALSE(condition) |
condition为假 |
切记,当它们失败时,ASSERT_*会产生一个致命失败,并会从当前的函数返回,当EXPECT_*产生一个非致命失败时,允许函数继续执行。但无论哪种失败,它都表示测试中有失败的测试语句。
这节介绍一些比较两个值的断言。
致命断言 |
非致命断言 |
验证 |
ASSERT_EQ(expected, actual) |
EXPECT_EQ(expected, actual) |
expected == actual |
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 |
在失败的情况下,Google Test会打印val1和val2的值。在使用ASSERT_EQ*和EXPECT_EQ*(以及随后要介绍的判断相等的断言时),你应该将你要测试的表达式写到actual的位置,将你期望的值写在expected的位置,因为这样做GTest的失败信息会根据这种规则打印。
你在断言要中所要进行的比较操作,必须是这种类型所支持的,否则你会得到一个编译错误。我们曾在v1.6.0版本之前要求参数支持<<操作符,但现在已经不再强制要求了(如果参数支持<<操作符,它会在断言失败时调用<<操作符打印参数值,如果不支持,GTest就尽它的所能打印参数了。如果想了解更多的自定义打印参数值,可以参见Google Mock Recipe)。
这些断言支持用户自定义的类型,但你必须支持相应的比较操作符(比如:==, <等等)。如果定义了相应的操作符,用ASSERT_*()是一个更好的选择,因为这些断言不仅会打印比较的结果,还会打印两个操作数。
参数总是只在断言中判断一次。所以参数有副作用是没有问题的。(译注:比如EXPECT_EQ(5, ++i),这里i是有副作用的,它的值会加1)。但是,如像在任何c/c++函数中一样,参数的比较顺序是不定的(编译器可以自由地选择任意顺序),所以你的代码不应该依赖特定的参数比较顺序。(译注:比如:EXPECT_EQ(++i + 5, value + i),这里先执行++i + 5,与先执行value + i的比较结果是不同的)。
ASSERT_EQ()可以进行指针所指向的值比较。但如果在比较两个c字符串时,它只判断两个指针是否指向同一内存地址,而不是比较内容。所以如果你想比较两个c字符串的值,你应该使用ASSERT_STREQ(),下一节会介绍。如果在判断c字符是否为NULL,可以写ASSERT_EQ(NULL, c_string)。但是,如果比较两个string对象,你应该用ASSERT_EQ。
这节所介绍的宏可以用于比较string或是wstring对象。
这节介绍比较两个c字符串的断言。如果你想比较两个string对象,你可以用EXPECT_EQ, EXPECT_NE等等。
致命断言 |
非致命断言 |
验证 |
ASSERT_STREQ(expected_str, actual_str); |
EXPECT_STREQ(expected_str, actual_str); |
两个c字符串是否内容相同 |
ASSERT_STRNE(str1, str2) |
EXPECT_STRNE(str1, str2) |
两个c字符串是否内容不同 |
ASSERT_STRCASEEQ(expected_str, actual_str); |
EXPECT_STRCASEEQ(expected_str, actual_str); |
两个c字符串是否内容相同,忽略大小写 |
ASSERT_STRCASEEQ(str1, str2); |
EXPECT_STRCASEEQ(str1, str2); |
两个c字符串是否内容不同,忽略大小写 |
注意:断言名称中的CASE表示忽略大小写。
*STREQ*和*STRNE*同样接受宽c字符串参数(wchar_t*),如果比较两个宽字符串失败,它们的值会以UTF-8窄字符串方式打印。
一个空指针和空字符串在断言判断时是被认为是不相等的。
更多的字符串比较技巧(子串,前缀,后缀,正式表达式匹配等等),可以在Advanced Google Test Guide中找到。
创建一个测试的步骤:
1. 用TEST()宏定义一个测试函数名,所定义的这个函数是没有返回值的普通函数。
2. 在这个函数中,你可以写任意合法的c++语句,并用GTest的断言来验证一些变量值。
3. 测试的结果由断言决定,如果测试中的任一断言失败(无论是致命断言或是非致命断言),或是测试崩溃,则整个测试失败。否则,测试成功。
TEST(test_case_name, test_name) {
... test body ...
}
TEST()中的第一个参数是测试用例的名字,第二个参数是测试函数的名字。两个名字中都必须是合法的C++函数名字符,并且不应该包含下划线(_)。一个测试函数的完整名称包括测试用例的名称和它自身名称。不同测试用例中的测试函数可以有相同的名称。
下面以一个简单的函数为例:
int Factorial(int n); //返回n的阶乘
对这个函数的测试用例可能是这样的:
// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
EXPECT_EQ(1, Factorial(0));
}
// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}
Google Test以测试用例的方式将测试函数组织起来,所以逻辑上相关的测试函数应该属于同一测试用例,换言之,这些测试函数的TEST()中的第一个参数应该是相同的。在前面的例子中,我们有两个测试函数,HandleZeroInput和HandlePositiveInput,它们属于同一测试用例FactorialTest。
如果你发现你在写多个测试函数时要进行相似的数据操作,你可以用一个test fixture。它允许你在不同的测试函数中重用一个相同的对象。
创建一个fixture,仅需要:
1. 创建一个继承自::testing::Test的类。将类成员声明为public:或是protected:,因为我们想要从子类中访问这些成员。
2. 你可以在类中声明你想使用的任何对象。
3. 如果有需要,你可以在默认构造函数或是SetUp()函数中对成员变量进行初始化,一个常见的错误是拼写SetUp()时写成了Setup(),请你不要再犯这个错。
4. 如果有必要,在析构函数或是TearDown()函数中对释放在SetUp()中申请的资源。要了解何时使用构造函数/析构函数,何时使用SetUp()/TearDown(),参见FAQ。
当使用一个fixture,你要使用TEST_F()而不是TEST(),TEST_F()允许你访问test fixture中的对象和函数。
TEST_F(test_case_name, test_name) {
... test body ...
}
如在TEST()中一样,第一个参数是测试用例的名称,但对TEST_F()来说,第一个参数必须是test fixture的类名。正如你所猜测的一样,_F是指fixture
不幸的是,C++宏系统不允许我们只用一个宏来表示这两种测试类型。如果你用错了宏,会得到一个编译错误。
并且你必须先定义一个fixture类,然后再在TEST_F()中使用,否则你会得到一个编译错误“`virtual outside class decleartion`”。
对于每个用TEST_F()定义的测试函数,GTest会:
1. 在运行时创建一个全新的test fixture。
2. 立即用SetUp()函数初始化
3. 运行测试函数
4. 用TearDown()函数进行清理
5. 删除test fixture对象。注意在同一测试用例中的测试函数有着不同的test fixture对象,并且GTest在创建一个新的test fixture之前总是会删除前一test fixture。GTest在在不同的测试函数中重用同一test fixture。所以无论你在一个测试函数中对fixture做什么都不会影响另一测试函数。
我们这里以一个名为Queue的FIFO队列为例进行测试,Queue的接口如下:
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类,传统习惯是如果你测试Foo类,你的测试类应该名为FooTest。
class QueueTest : public ::testing::Test {
protected:
virtual void SetUp() {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// virtual void TearDown() {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
在这个测试用例中,不需要写TearDown()函数,因为我们在每个测试函数后不需要清理,并且最后的资源释放是由默认析构函数来完成。
现在我们来看使用TEST_F()和fixture的测试函数。
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(0, q0_.size());
}
TEST_F(QueueTest, DequeueWorks) {
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(2, *n);
EXPECT_EQ(1, q2_.size());
delete n;
}
上面的例子ASSERT_*和EXPECT_*两种断言都有使用。使用断言的经验法则是:如果你想继续执行测试函数,以显示失败断言之后的更多错误,就用EXPECT_*,而使用ASSERT_*是如果断言失败后再继续执行测试函数则毫无意义。比如,在Dequeque的测试中第二个断言,ASSERT_TRUE(n != NULL),因为需要对n解引用,如果n是NULL,继续执行会产生段错误。
当这些测试运行时,以下的步骤会发生:
1. GTest会构造一个QueueTest对象(我们称其为t1)。
2. t1.SetUp()初始化t1。
3. 第一个测试函数(IsEmptyInitially)运行时使用t1对象。
4. t1.TearDown()在测试函数完成后进行清理工作。
5. t1析构。
6. 上述步骤将会在另一个QueueTest对象上重复,这次将进行DequeueWorks测试。
注意:GTest会在一个测试对象构造时保存所有的Google Test标志,在对象析构时恢复这些标志。
TEST()和TEST_F()会隐式地注册测试函数。 所以不同于其它的C++测试框架,你不用了为运行它们,再显式地再把你定义的测试函数列一次。
在你定义你的测试之后,你可以用RUN_ALL_TESTS()运行它们,如果所有的测试函数都成功,它返回0,否则返回1.注意RUN_ALL_TESTS()运行你链接单元中的所有测试函数,这些测试函数可以来自不同的测试用例,甚至来自不同的源文件。
当调用RUN_ALL_TESTS()宏时:
1. 保存所有GTest的标志。
2. 为第一个测试函数创建一个test fixture。
3. 用SetUp()初始化它。
4. 使用test fixture对象运行测试函数。
5. 用TearDown()清理test fixture对象。
6. 删除test fixture。
7. 恢复所有的GTest标志状态。
8. 重复上述步骤,直到所有测试函数运行完。
另外,如果第二步,test fixture的构造函数产生了一个致命错误,也就没有必要执行第3~5步了。类似的,如果第3步产生了一个致命错误,也就不会执行第4步。
注意:你不能忽略RUN_ALL_TESTS()的返回值,否则gcc会给出一个编译错误。这样设计的原因是自动化测试服务是通过这个宏的返回值来判断一个测试是否通过,还不是根据在标准输出/标准错误输出上的输出。所以你的main()函数必须返回RUN_ALL_TESTS()的值。
你应该仅调用一次RUN_ALL_TESTS(),多次调用它会与一些GTest高级特性冲突(比如,thread-safe death测试),所以多次调用是不支持的。
下面是使用GTest的模板代码:
#include "this/package/foo.h"
#include "gtest/gtest.h"
namespace {
// The fixture for testing class Foo.
class FooTest : public ::testing::Test {
protected:
// You can remove any or all of the following functions if its body
// is empty.
FooTest() {
// You can do set-up work for each test here.
}
virtual ~FooTest() {
// 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:
virtual void SetUp() {
// Code here will be called immediately after the constructor (right
// before each test).
}
virtual void TearDown() {
// Code here will be called immediately after each test (right
// before the destructor).
}
// Objects declared here can be used by all tests in the test case for Foo.
};
// Tests that the Foo::Bar() method does Abc.
TEST_F(FooTest, MethodBarDoesAbc) {
const string input_filepath = "this/package/testdata/myinputfile.dat";
const string output_filepath = "this/package/testdata/myoutputfile.dat";
Foo f;
EXPECT_EQ(0, f.Bar(input_filepath, output_filepath));
}
// Tests that Foo does Xyz.
TEST_F(FooTest, DoesXyz) {
// Exercises the Xyz feature of Foo.
}
} // namespace
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
::testing::InitGoogleTest()函数解析设置GTest标志的命令行,并移除所有可识别的标志。这允许用户通过不同的标志来控制一个测试程序的行为,我们会在AdvancedGuide中讨论它。你必须在调用RUN_ALL_TESTS()之前调用它,否则标志不会正常初始化。
在Windows上,InitGoogleTest()支持宽字符,所以它们用于在UNICODE模式下编译的程序。
你是不是感觉写main()函数对你来说都太过麻烦?我们完全理解你的感受,这就是GTest提供一个main()函数的简单实现。如果它满足你的需求,你只需要将你的测试链接gtest_main库,你就可以不用再写main()了。
略
恭喜!你已经学习了GTest的基础。你可以开始编写并运行GTest测试了,阅读一些samples或是Advanced Guide,你会掌握更多有用的GTest特性。
GTest是设计为线程安全的。在可使用pthread库的系统上,实现是线程安全的。当前在其它系统上(比如:windows)两个进程并发不是安全的。在大多线测试中,这不是个问题,因为判断通常都是在主线程中完成的。如果你想提供帮助,你可以志愿在为你所用的平台去在gtest-port.h中添加必要的线程同步原语。