Google Test(GTest)使用方法和源码解析——预处理技术分析和应用

预处理

        在《Google Test(GTest)使用方法和源码解析——概况》最后一部分,我们介绍了GTest的预处理特性。现在我们就详细介绍该特性的使用和相关源码。(转载请指明出于breaksoftware的csdn博客)

测试特例级别预处理

        Test Fixtures是建立一个固定/已知的环境状态以确保测试可重复并且按照预期方式运行的装置。通过它,我们可以实现测试特例级别和之后介绍的测试用例级别的预处理逻辑。

        举一个比较常见的例子:我们要测试向数据库插入(id,name,location)这样的三个数据,那要先构建一个基础数据(0,Fang,Beijing)。我们第一个测试特例可能需要关注于id这个字段,于是它要在基础数据上做出修改,将(1,Fang,Beijing)插入数据库。第二个测试特例可能需要关注于name字段,于是它要在基础数据上做出修改,将(0,Wang,Beijing)插入数据库。第三个测试特例可能需要关注于location字段,于是它要修改基础数据,将(0,Fang,Nanjing)插入数据库。如果做得鲁莽点,我们在每个测试特例前,先将所有数据填充好,再去操作。但是如果我们将其提炼一下,其实我们发现我们只要在每个特例执行前,获取一份基础数据,然后修改其中本次测试关心的一项就可以了。同时这份基础数据不可以在每个测试特例中被修改——即本次测试特例获取的基础数据不会受之前测试特例对基础数据修改而影响——获取的是一个恒定的数据。

        我们看下Test Fixtures类定义及使用规则:

  1. Test Fixtures类继承于::testing::Test类。
  2. 在类内部使用public或者protected描述其成员,为了保证实际执行的测试子类可以使用其成员变量(这个我们后面会分析下)
  3. 在构造函数或者继承于::testing::Test类中的SetUp方法中,可以实现我们需要构造的数据。
  4. 在析构函数或者继承于::testing::Test类中的TearDown方法中,可以实现一些资源释放的代码(在3中申请的资源)。
  5. 使用TEST_F宏定义测试特例,其第一个参数要求是1中定义的类名;第二个参数是测试特例名。

        其中4这步并不是必须的,因为我们的数据可能不是申请来的数据,不需要释放。还有就是“构造函数/析构函数”和“SetUp/TearDown”的选择,对于什么时候选择哪对,本文就不做详细分析了,大家可以参看https://github.com/google/googletest/blob/master/googletest/docs/FAQ.md#should-i-use-the-constructordestructor-of-the-test-fixture-or-the-set-uptear-down-function。一般来说就是构造/析构函数里忌讳做什么就不要在里面做,比如抛出异常等。

        我们以一个例子来讲解

class TestFixtures : public ::testing::Test {
public:
    TestFixtures() {
        printf("\\nTestFixtures\\n");
    };
    ~TestFixtures() {
        printf("\\n~TestFixtures\\n");
    }
protected:
    void SetUp() {
        printf("\\nSetUp\\n");
        data = 0;
    };
    void TearDown() {
        printf("\\nTearDown\\n");
    }
protected:
    int data;
};

TEST_F(TestFixtures, First) {
    EXPECT_EQ(data, 0);
    data =  1;
    EXPECT_EQ(data, 1);
}

TEST_F(TestFixtures, Second) {
    EXPECT_EQ(data, 0);
    data =  1;
    EXPECT_EQ(data, 1);
}

        First测试特例中,我们修改了data的数据(23行),第24行验证了修改的有效性和正确性。在second的测试特例中,一开始就检测了data数据(第28行),如果First特例中修改data(23行)影响了基础数据,则本次检测将失败。我们将First和Second测试特例的实现定义成一样的逻辑,可以避免编译器造成的执行顺序不确定从而影响测试结果。我们看下测试输出

[----------] 2 tests from TestFixtures
[ RUN      ] TestFixtures.First
TestFixtures
SetUp
TearDown
~TestFixtures
[       OK ] TestFixtures.First (9877 ms)
[ RUN      ] TestFixtures.Second
TestFixtures
SetUp
TearDown
~TestFixtures
[       OK ] TestFixtures.Second (21848 ms)
[----------] 2 tests from TestFixtures (37632 ms total)

        可以见得,所有局部测试都是正确的,验证了Test Fixtures类中数据的恒定性。我们从输出应该可以看出来,每个测试特例都是要新建一个新的Test Fixtures对象,并在该测试特例结束时销毁它。这样可以保证数据的干净。
        我们来看下其实现的源码,首先我们看下TEST_F的实现

#define TEST_F(test_fixture, test_name)\\
  GTEST_TEST_(test_fixture, test_name, test_fixture, \\
              ::testing::internal::GetTypeId<test_fixture>())

       我们再回顾下在《Google Test(GTest)使用方法和源码解析——自动调度机制分析》中分析的TEST宏的实现

#define GTEST_TEST(test_case_name, test_name)\\
  GTEST_TEST_(test_case_name, test_name, \\
              ::testing::Test, ::testing::internal::GetTestTypeId())

        可以见得它们的区别就是声明的测试特例类继承于不同的父类。同时使用的是public继承方式,所以子类可以使用父类的public和protected成员。这也是我们在介绍Test Fixtures类编写规则时说的,让使用到的变量置于protected域之下的原因。

#define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\\
class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\\

        我们再看下Test Fixtures类对象在框架中是怎么创建、使用和销毁的。

        在TestInfo::Run()函数中有Test Fixtures对象和销毁的代码

  // Creates the test object.
  Test* const test = internal::HandleExceptionsInMethodIfSupported(
      factory_, &internal::TestFactoryBase::CreateTest,
      "the test fixture's constructor");

  // Runs the test only if the test object was created and its
  // constructor didn't generate a fatal failure.
  if ((test != NULL) && !Test::HasFatalFailure()) {
    // This doesn't throw as all user code that can throw are wrapped into
    // exception handling code.
    test->Run();
  }

  // Deletes the test object.
  impl->os_stack_trace_getter()->UponLeavingGTest();
  internal::HandleExceptionsInMethodIfSupported(
      test, &Test::DeleteSelf_, "the test fixture's destructor");

        因为测试特例类继承于Test Fixtures类,Test Fixtures类继承于Test类,所以我们可以通过厂类生成一个Test类对象的指针,这就是它创建的过程。在测试特例运行结束后,第16~17行将销毁该对象。

        在Test类的Run方法中,除了调用了子类定义的虚方法,还执行了SetUp和TearDown方法

  internal::HandleExceptionsInMethodIfSupported(this, &Test::SetUp, "SetUp()");
  // We will run the test only if SetUp() was successful.
  if (!HasFatalFailure()) {
    impl->os_stack_trace_getter()->UponLeavingGTest();
    internal::HandleExceptionsInMethodIfSupported(
        this, &Test::TestBody, "the test body");
  }

  // However, we want to clean up as much as possible.  Hence we will
  // always call TearDown(), even if SetUp() or the test body has
  // failed.
  impl->os_stack_trace_getter()->UponLeavingGTest();
  internal::HandleExceptionsInMethodIfSupported(
      this, &Test::TearDown, "TearDown()");

测试用例级别预处理

        这种预处理方式也是要使用Test Fixtures。不同的是,我们需要定义几个静态成员:

  1. 静态成员变量,用于指向数据。
  2. 静态方法SetUpTestCase()
  3. 静态方法TearDownTestCase()

       举个例子,我们需要自定义测试用例开始和结束时的行为

  • 测试开始时输出Start Test Case
  • 测试结束时统计结果
class TestFixturesS : public ::testing::Test {
public:
    TestFixturesS() {
        printf("\\nTestFixturesS\\n");
    };
    ~TestFixturesS() {
        printf("\\n~TestFixturesS\\n");
    }
protected:
    void SetUp() {
    };
    void TearDown() {
    };

    static void SetUpTestCase() {
        UnitTest& unit_test = *UnitTest::GetInstance();
        const TestCase& test_case = *unit_test.current_test_case();
        printf("Start Test Case %s \\n", test_case.name());
    };

    static void TearDownTestCase() {
        UnitTest& unit_test = *UnitTest::GetInstance();
        const TestCase& test_case = *unit_test.current_test_case();
        int failed_tests = 0;
        int suc_tests = 0;
        for (int j = 0; j < test_case.total_test_count(); ++j) {
            const TestInfo& test_info = *test_case.GetTestInfo(j);
            if (test_info.result()->Failed()) {
                failed_tests++;
            }
            else {
                suc_tests++;
            }
        }
        printf("End Test Case %s. Suc : %d, Failed: %d\\n", test_case.name(), suc_tests, failed_tests);
    };

};

TEST_F(TestFixturesS, SUC) {
    EXPECT_EQ(1,1);
}

TEST_F(TestFixturesS, FAI) {
    EXPECT_EQ(1,2);
}

        测试用例中,我们分别测试一个成功结果和一个错误的结果。然后输出如下

[----------] 2 tests from TestFixturesS
Start Test Case TestFixturesS
[ RUN      ] TestFixturesS.SUC
TestFixturesS
~TestFixturesS
[       OK ] TestFixturesS.SUC (2 ms)
[ RUN      ] TestFixturesS.FAI
TestFixturesS
..\\test\\gtest_unittest.cc(126): error:       Expected: 1
To be equal to: 2
~TestFixturesS
[  FAILED  ] TestFixturesS.FAI (5 ms)
End Test Case TestFixturesS. Suc : 1, Failed: 1
[----------] 2 tests from TestFixturesS (12 ms total)

        从输出上看,SetUpTestCase在测试用例一开始时就被执行了,TearDownTestCase在测试用例结束前被执行了。我们看下源码中怎么实现的

// Runs every test in this TestCase.
void TestCase::Run() {
......
  internal::HandleExceptionsInMethodIfSupported(
      this, &TestCase::RunSetUpTestCase, "SetUpTestCase()");
......
  for (int i = 0; i < total_test_count(); i++) {
    GetMutableTestInfo(i)->Run();
  }
......
  internal::HandleExceptionsInMethodIfSupported(
      this, &TestCase::RunTearDownTestCase, "TearDownTestCase()");
......
}

        代码之前了无秘密,以上节选的内容可以说明其执行的先后关系以及执行的区域。

全局级别预处理

        顾名思义,它是在测试用例之上的一层初始化逻辑。如果我们要使用该特性,则要声明一个继承于::testing::Environment的类,并实现其SetUp/TearDown方法。这两个方法的关系和之前介绍Test Fixtures类是一样的。

        我们看一个例子,我们例子中的预处理

  • 测试开始时输出Start Test
  • 测试结束时统计结果
namespace testing {
namespace internal {
class EnvironmentTest : public ::testing::Environment {
public:
    EnvironmentTest() {
        printf("\\nEnvironmentTest\\n");
    };
    ~EnvironmentTest() {
        printf("\\n~EnvironmentTest\\n");
    }
public:
    void SetUp() {
        printf("\\n~Start Test\\n");
    };
    void TearDown() {
        UnitTest& unit_test = *UnitTest::GetInstance();
        for (int i = 0; i < unit_test.total_test_case_count(); ++i) {
            int failed_tests = 0;
            int suc_tests = 0;
            const TestCase& test_case = *unit_test.GetTestCase(i);
            for (int j = 0; j < test_case.total_test_count(); ++j) {
                const TestInfo& test_info = *test_case.GetTestInfo(j);
                // Counts failed tests that were not meant to fail (those without
                // 'Fails' in the name).
                if (test_info.result()->Failed()) {
                    failed_tests++;
                }
                else {
                    suc_tests++;
                }
            }
            printf("End Test Case %s. Suc : %d, Failed: %d\\n", test_case.name(), suc_tests, failed_tests);
        }
    };
};
}
}

GTEST_API_ int main(int argc, char **argv) {
  printf("Running main() from gtest_main.cc\\n");
  ::testing::AddGlobalTestEnvironment(new testing::internal::EnvironmentTest);
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

        EnvironmentTest的代码我们就不讲解了,我们可以关注下::testing::AddGlobalTestEnvironment(new testing::internal::EnvironmentTest);这句,我们要在调用RUN_ALL_TESTS之前,使用该函数将全局初始化对象加入到框架中。通过这种方式,可以猜测出,我们可以加入多个对象到框架中。我们看下源码中对它们的调度

bool UnitTestImpl::RunAllTests() {
........
      ForEach(environments_, SetUpEnvironment);
........

      // Runs the tests only if there was no fatal failure during global
      // set-up.
      if (!Test::HasFatalFailure()) {
        for (int test_index = 0; test_index < total_test_case_count();
             test_index++) {
          GetMutableTestCase(test_index)->Run();
        }
      }
........
      std::for_each(environments_.rbegin(), environments_.rend(),
                    TearDownEnvironment);
........
}
static void SetUpEnvironment(Environment* env) { env->SetUp(); }
static void TearDownEnvironment(Environment* env) { env->TearDown(); }

        截取的源码已经解释的很清楚了。我们看到environments_是个容器,这也印证了我们对于框架中可以有多个Environment的预期。

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

昵称

取消
昵称表情代码图片

    暂无评论内容