GTest是很多开源工程的测试框架。虽然介绍它的博文非常多,但是我觉得可以深入到源码层来解析它的实现原理以及使用方法。这样我们不仅可以在开源工程中学习到实用知识,还能学习到一些思想和技巧。我觉得有时候思想和技巧是更重要的。(转载请指明出于breaksoftware的csdn博客)
我们即将要分析的是GTest1.7版本。我们可以通过https://github.com/google/googletest.git得到代码。
官方文档见:
我们先大致熟悉一下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有 自动统计结果、 自动格式化输出结果、 自动调度执行等特性。这些特性也将是之后博文分析的重点。
如果我们编写的测试用例组(如上例是两组)中一组发生了错误,我们希望没出错的那组不用执行了,出错的那组再执行一遍。一般情况下,我们可能需要去删除执行正确的那段测试代码,但是这种方式非常不优美——需要编译或者忘记恢复代码。GTest框架可以让我们通过在程序参数控制执行哪个测试用例,比如我们希望只执行Factorial测试,就可以这样调用程序
./sample1_unittest --gtest_filter=Factorial*我们可以将以上特性称之为 选择性测试。
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宏的第一个参数我们可以随便命名。