GTest是很多开源工程的测试框架。虽然介绍它的博文非常多,但是我觉得可以深入到源码层来解析它的实现原理以及使用方法。这样我们不仅可以在开源工程中学习到实用知识,还能学习到一些思想和技巧。我觉得有时候思想和技巧是更重要的。(转载请指明出于breaksoftware的csdn博客)
我们即将要分析的是GTest1.7版本。我们可以通过https://github.com/google/googletest.git得到代码。
官方文档见:
- https://github.com/google/googletest/blob/master/googletest/docs/primer.md
- https://github.com/google/googletest/blob/master/googletest/docs/advanced.md
我们先大致熟悉一下GTest的特性。GTest和很多开源工程一样,并不只是针对特定的平台,否则其使用范围将大打折扣,所以GTest具有很好的移植特性和可复用性,我们以工程中的代码为例
template <class T, typename Result>
Result HandleSehExceptionsInMethodIfSupported(
T* object, Result (T::*method)(), const char* location) {
#if GTEST_HAS_SEH
__try {
return (object->*method)();
} __except (internal::UnitTestOptions::GTestShouldProcessSEH( // NOLINT
GetExceptionCode())) {
std::string* exception_message = FormatSehExceptionMessage(
GetExceptionCode(), location);
internal::ReportFailureInUnknownLocation(TestPartResult::kFatalFailure,
*exception_message);
delete exception_message;
return static_cast<Result>(0);
}
#else
(void)location;
return (object->*method)();
#endif // GTEST_HAS_SEH
}
这段代码只是为了执行模板类T对象的method函数指针指向的方法。其核心就是(object->*method)()这句,但是它却使用了20行的代码去实现,就是为了解决平台的兼容问题。从名字我们可以看出它为了兼容SEH机制——结构化异常处理——一种windows系统上提供给用户处理异常的机制。而这种机制在linux系统上没有。这个函数是GTest为移植特性所做工作的一个很好的代表,我们将在之后的源码介绍中经常见到它的身影。
我们编码时,有时候我们不仅考究逻辑的严谨,还非常注意编码的风格和布局的优美。其实代码就像一件作品,一个不负责任的作者可能是毫无章法的涂鸦手法,而有些有着一定境界的程序员可能就会按照自己的“画风”去绘制——他的“画风”可能你并不喜欢,但是那种风格却是独立和鲜明的,甚至是有一定道理的。而且如果一旦“画风”确定,对于临摹者来说只要照着这样的套路去做,而不用自己发挥自己的风格,这对一个库的发展也是非常有益的。 GTest同样有着良好的组织结构,我们以其自带的Sample1为例
// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}
// Tests factorial of 0.
TEST(FactorialTest, Zero) {
EXPECT_EQ(1, Factorial(0));
}
// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}
这段代码是一套完整的测试代码。可以观察发现,每个逻辑使用一个TEST宏控制,其内部也是一系列EXPECT_*宏堆砌。先不论其他风格,单从整齐有规律的书写方式上来说,GTest也算是一个便于结构性编码的样板。我们使用者只要照着这样的样板去编写测试用例,是非常方便的,这也将大大降低我们使用GTest库的门槛。
TEST宏是一个很重要的宏,它构成一个测试特例。现在有必要介绍下其构成,TEST宏的第一个参数是“测试用例名”,第二个参数是“测试特例名”。测试用例(Test Case)是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求(引百度百科),测试特例是测试用例下的一组测试。以以上代码为例,三段TEST宏构成的是一个测试用例——测试用例名是FactorialTest(阶乘方法检测,测试Factorial函数),该用例覆盖了三种测试特例——Negative、Zero和Positive——即检测输入参数是负数、0和正数这三种特例情况。
我们再看一组检测素数的测试用例
TEST(IsPrimeTest, Negative) {
// This test belongs to the IsPrimeTest test case.
EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}
// Tests some trivial cases.
TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}
// Tests positive input.
TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
这组测试用例的名是IsPrimeTest(测试IsPrime函数),三个测试特例是Negative(错误结果场景)、Trivial(有对有错的场景)和Positive(正确结果场景)。
对于测试用例名和测试特例名,不能有下划线(_)。因为GTest源码中需要使用下划线把它们连接成一个独立的类名
// Expands to the name of the class that implements the given test.
#define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \\
test_case_name##_##test_name##_Test
这样也就要求,我们不能有相同的“测试用例名和特例名”的组合——否则类名重合。
测试用例名和测试特例名的分开,使得我们编写的测试代码有着更加清晰的结构——即有相关性也有独立性。相关性是通过相同的测试用例名联系的,而独立性通过不同的测试特例名体现的。我们通过这段测试代码的运行结果查看一下这两个特性
Running main() from gtest_main.cc
[==========] Running 6 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN ] FactorialTest.Negative
[ OK ] FactorialTest.Negative (0 ms)
[ RUN ] FactorialTest.Zero
[ OK ] FactorialTest.Zero (0 ms)
[ RUN ] FactorialTest.Positive
[ OK ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (0 ms total)
[----------] 3 tests from IsPrimeTest
[ RUN ] IsPrimeTest.Negative
[ OK ] IsPrimeTest.Negative (0 ms)
[ RUN ] IsPrimeTest.Trivial
[ OK ] IsPrimeTest.Trivial (0 ms)
[ RUN ] IsPrimeTest.Positive
[ OK ] IsPrimeTest.Positive (0 ms)
[----------] 3 tests from IsPrimeTest (0 ms total)
[----------] Global test environment tear-down
[==========] 6 tests from 2 test cases ran. (14 ms total)
[ PASSED ] 6 tests.
从输出结果上,我们看到GTest框架将我们相同测试用例名的场景合并在一起,不同测试特例名的场景分开展现。而且我们还发现GTest有自动统计结果、自动格式化输出结果、自动调度执行等特性。这些特性也将是之后博文分析的重点。
虽然上例中,所有的执行都是正确的,但是如果以上测试中发生一个错误,也不能影响其他测试——不同测试用例不相互影响、相同测试用例不同测试特例不相互影响。我们称之为独立性。除了独立性,也不失灵活性——一个测试测试特例中可以通过不同宏(ASSERT_*类宏会影响之后执行,EXPECT_*类宏不会)控制是否影响之后的执行。
如果我们编写的测试用例组(如上例是两组)中一组发生了错误,我们希望没出错的那组不用执行了,出错的那组再执行一遍。一般情况下,我们可能需要去删除执行正确的那段测试代码,但是这种方式非常不优美——需要编译或者忘记恢复代码。GTest框架可以让我们通过在程序参数控制执行哪个测试用例,比如我们希望只执行Factorial测试,就可以这样调用程序
./sample1_unittest --gtest_filter=Factorial*
我们可以将以上特性称之为选择性测试。
最后一个特性便是预处理。我们测试时,往往要构造复杂的数据。如果我们在每个测试特例中都要构造一遍数据,将是非常繁琐和不美观的。GTest提供了一种提前构建数据的方式。我们以如下代码为例
class ListTest : public testing::Test {
protected:
virtual void SetUp() {
_m_list[0] = 11;
_m_list[1] = 12;
_m_list[2] = 13;
}
int _m_list[3];
};
TEST_F(ListTest, FirstElement) {
EXPECT_EQ(11, _m_list[0]);
}
TEST_F(ListTest, SecondElement) {
EXPECT_EQ(12, _m_list[1]);
}
TEST_F(ListTest, ThirdElement) {
EXPECT_EQ(13, _m_list[2]);
}
我们让ListTest类继承于GTest提供的基类testing::Test,并重载SetUp方法。这样我们每次执行ListTest的一个测试特例时,SetUp方法都会执行一次,从而将数据准备完毕。这样我们只要在一个类中构建好数据就行了。这儿需要注意一下TEST_F宏,它的第一参数要求是类名——即ListTest——不像TEST宏的第一个参数我们可以随便命名。
暂无评论内容