Google Test(GTest)使用方法和源码解析——概况

        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有自动统计结果自动格式化输出结果自动调度执行等特性。这些特性也将是之后博文分析的重点。     

        虽然上例中,所有的执行都是正确的,但是如果以上测试中发生一个错误,也不能影响其他测试——不同测试用例不相互影响、相同测试用例不同测试特例不相互影响。我们称之为独立性。除了独立性,也不失灵活性——一个测试测试特例中可以通过不同宏(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宏的第一个参数我们可以随便命名。

© 版权声明
THE END
喜欢就支持一下吧
点赞84 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容