白盒测试体系—框架搭建篇

2020-05-08 16:01:20 浏览数 (1)

良好的布局是成功的一半,选择一款合适的测试框架能够使我们的白盒测试更高效,事半功倍。

本文就我们过去三年在搭建测试框架中积攒的一些经验给大家做个简单的分享,主要从以下5个方面展示:

  • 什么是测试框架
  • 为什么要用测试框架
  • 哪些测试项目适合搭建测试框架
  • 如何搭建测试框架
  • 什么时候搭建测试框架

1

什么是测试框架

【简介】

测试框架是测试开发过程中提取特定领域测试方法共性部分形成的体系结构,并不是一个现成可用的系统,需要测试工程师在它基础上结合自己的测试对象转换为自己的测试用例。

【测试框架四要素】

测试框架通用的有四元素:

1.测试目标

一般是一个函数,一个对象或者一组相关的对象集。测试人员在测试前需要对测试目标有充分的了解,明确测试的预期结果。

2.测试集

这一组测试用例服务于相同的测试目标,保证测试的有序维护。

3.测试执行

测试集按序执行

4.断言

验证被测程序在测试中的行为或状态是否符合预期。

2

为什么要用测试框架

【提高效率】

在测试框架基础上重用测试设计原则和测试经验,调整部分内容便可满足需求,可提高测试用例设计开发质量,降低成本,缩短时间。

3

哪些项目适用测试框架

【逻辑复杂且封装性好】

首先如果代码逻辑很简单,单测也就没有太大的必要了;其次如果代码没有进行基本的封装或者封装过度,层次结构不清晰,那在测试过程中也是举步维艰。

【复用性高】

被测试模块的复用性高,搭建的测试框架才是有价值有收益的,毕竟投入成本很高;其次在测试中还可以抽象出可重复使用的公共方法,测试代码的复用性也高。

4

如何搭建测试框架

【框架选择】

测试框架种类繁多,我们选择时满足以下特点即可:

  • 开源
  • 功能强大
  • 扩展性好

常用测试框架:

  • Java - JUnit/TestNG
  • .Net - NUnit
  • C - CPPUnit/GTest
  • Python - pyUnit/py.test/Nose
  • OC - OCUnit/OCMock

【基本执行过程】

一般测试用例执行过程分为四个步骤:

SetUp:准备阶段

每个测试用例执行前的准备阶段,部署测试环境比如对象的初始化等。

Run:测试执行

执行测试用例。

Verify:测试验证

验证测试用例的结果是否符合预期。

TearDown:清理环境

清理该条测试用例执行中产生的环境,比如申请空间的释放,还原测试环境,保证对其他的测试用例无连带影响。

代码语言:javascript复制
int foo(int a, int b)
{
    return a   b;
}
 
class MyTest : public testing::Test
{
public:
    virtual void SetUp(){}
    virtual void TearDown(){}
};
 
TEST_F(MyTest, normal)
{
    ASSERT_EQ(2, foo(1, 1));
}
 
int main(int argc, char const *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    RUN_ALL_TESTS();
    return 0;
}

【框架运行机制】

我们使用一种测试框架时,只有当你知道框架内部是如何运行的,不仅知其然,还知其所以然,才能使用的更加得心应手。

就拿上述gtest框架的一个最简单的测试demo说明吧:

【TEST_F宏】

我们从测试用例的第一行TEST_F(MyTest, normal)解析,TEST_F的类调用了GTEST_TEST_宏

代码语言:javascript复制
#define TEST_F(test_fixture, test_name)
  GTEST_TEST_(test_fixture, test_name, test_fixture, 
              ::testing::internal::GetTypeId<test_fixture>())

从GTEST_TEST_宏的定义可知:

该用例生成一个类GTEST_TEST_CLASS_NAME_(test_case_name, test_name),类名通过源代码解析为test_case_name和test_name以及_Test的拼接,即MyTest_normal_Test类。

代码语言:javascript复制
#define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) 
  test_case_name##_##test_name##_Test
代码语言:javascript复制
#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 {
 public:
  GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}
 private:
  virtual void TestBody();
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
  GTEST_DISALLOW_COPY_AND_ASSIGN_(
      GTEST_TEST_CLASS_NAME_(test_case_name, test_name));
};

::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)
  ::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        #test_case_name, #test_name, NULL, NULL, 
        ::testing::internal::CodeLocation(__FILE__, __LINE__), 
        (parent_id), 
        parent_class::SetUpTestCase, 
        parent_class::TearDownTestCase, 
        new ::testing::internal::TestFactoryImpl<
            GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);
void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()

该测试用例类的结构如下:

代码语言:javascript复制
class MyTest_normal_Test : public ::testing::Test
{
public:
    MyTest_normal_Test() {}
  
private:
    virtual void TestBody();
public:
    virtual void SetUp();
    virtual void TearDown();
};
::testing::TestInfo* const MyTest_normal_Test
  ::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
      "MyTest", "normal", __null, __null,
        (::testing::internal::GetTestTypeId()),
        ::testing::Test::SetUpTestCase,
        ::testing::Test::TearDownTestCase,
new ::testing::internal::TestFactoryImpl<MyTest_normal_Test>);

在TestInfo类中主要调用了MakeAndRegisterTestInfo函数,从函数名称可知该函数的作用是创建并注册TestInfo信息。

代码语言:javascript复制
TestInfo* MakeAndRegisterTestInfo(
    const char* test_case_name, //测试套名称,即TEST_F宏的第一个参数
    const char* name, //测试案例名称
    const char* type_param, //测试套的附加信息,默认为空
    const char* value_param, //测试案例的附加信息,默认为空
    CodeLocation code_location,
    TypeId fixture_class_id, //类id
    SetUpTestCaseFunc set_up_tc, //函数指针,指向函数SetUpTestCaseFunc
    TearDownTestCaseFunc tear_down_tc, //函数指针,指向函数TearDownTestCaseFunc
    TestFactoryBase* factory //指向工厂对象的指针,该工厂对象创建上面TEST宏生成的测试类的对象
    ) {
  TestInfo* const test_info =
      new TestInfo(test_case_name, name, type_param, value_param,
                   code_location, fixture_class_id, factory);
  GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
  return test_info;
}

我们看到在MakeAndRegisterTestInfo函数体中定义了一个TestInfo对象,该对象包含了一个TEST_F宏中标识的测试案例的测试套名称、测试案例名称、测试套附加信息、测试案例附加信息、创建测试案例类对象的工厂对象的指针这些信息。

通过对源码的抽丝剥茧,工厂对象UnitTestImpl类的AddTestInfo操作最终指向TestCase类,将包含测试用例信息的Test_info对象添加到test_info_list_中,而test_info_list_是类TestCase中的私有数据成员,它也是一个vector向量。

代码语言:javascript复制
class GTEST_API_ TestCase {
public:
	TestCase(const char* name, const char* a_type_param,
           Test::SetUpTestCaseFunc set_up_tc,
           Test::TearDownTestCaseFunc tear_down_tc);
	…… //省略
private:
	std::vector<TestInfo*> test_info_list_;
	…… //省略
}

【RUN_ALL_TESTS】

我们测试用例的执行从main函数中RUN_ALL_TESTS接口开始,该接口定义如下:

代码语言:javascript复制
inline int RUN_ALL_TESTS() {
  return ::testing::UnitTest::GetInstance()->Run();
}

UnitTest类的Run函数又是如何执行的呢,通过源码可以一步一步跟踪深入,这里篇幅所限就不一一展开了,核心关键部分最终调用了TestCase类的Run接口,这个类我们还有印象,在上文中提过,每一个测试用例产生的类信息存在TestCase的私有变量test_info_list_中

代码语言:javascript复制
void TestCase::Run() {
  ......  //省略
  const internal::TimeInMillis start = internal::GetTimeInMillis();
  for (int i = 0; i < total_test_count(); i  ) {
    GetMutableTestInfo(i)->Run(); //调用TestCase::GetMutableTestInfo
  }                                     //以及Test_Info::Run
  ...... //省略
}
TestInfo* TestCase::GetMutableTestInfo(int i) {
  const int index = GetElementOr(test_indices_, i, -1);
  return index < 0 ? NULL : test_info_list_[index];
}

TestInfo类的Run接口最终执行命令为Test类的Run接口,该接口源码如下:

代码语言:javascript复制
void Test::Run() {
  …… //省略
  if (!HasFatalFailure()) {
    impl->os_stack_trace_getter()->UponLeavingGTest();
    internal::HandleExceptionsInMethodIfSupported(
        this, &Test::TestBody, "the test body");
  }
  …… //省略
}

从代码中我们看到通过HandleExceptionsInMethodIfSupported调用了TestBody函数,而Test中TestBody的原型声明为:

代码语言:javascript复制
virtual void TestBody() = 0;

TestBody的声明原来为纯虚函数,拨云见日,执行过程终于识得庐山真面目。Test::Run接口调用了tesk::TestBody,而test实际上是继承了Test类的案例类对象,即MyTest_normal_Test,该类的TestBody实际执行的即为测试用例中的内容

如此循环执行,就是说gtest框架会顺序执行程序中的每一个TEST_F宏的函数体。

【总结】

简而言之,gtest的运行过程分为以下几步:

  1. 每一个TEST或者TEST_F宏生成一个测试案例类,继承自Test类
  2. 对于每一个测试案例类,由一个工厂类对象创建该类的对象
  3. 每一个测试案例类对象创建一个Test_Info对象
  4. Test_Info对象会创建一个TestCase对象的指针,存入vector向量中
  5. 对每一个TEST_F宏进行1-4步骤
  6. 整个项目中唯一的UnitTestImpl对象,能够按序获取到每一个测试案例对象的信息
  7. 执行RUN_ALL_TEST接口,依次遍历vector向量中的元素,最终调用相对应测试案例对象中的TestBody函数,即测试用例代码

5

什么时候搭建测试框架

【尽早开始】

自然在软件开发框架或模块接口确定后,测试人员便可以着手搭建测试框架,尽早的投入白盒测试,所以这里建议尽早开始,当然在项目的任何阶段我们都可以介入白盒测试,开始搭建测试框架在不同时期满足相应原则即可:

项目初期

满足功能测试的需求,可以快速地发现问题

项目中期

健壮,稳定执行,代码覆盖率逼近70%

项目后期

高效执行,方便进行持续集成敏捷开发

0 人点赞