本文共 13348 字,大约阅读时间需要 44 分钟。
【摘要】在本文中,作者根据之前使用gtest框架进行测试的经验,总结了一些使用方式和案例。 在这些案例中,我们可以了解到gtest框架的基本使用方法以及在我们日常测试中的应用,同时也能促进我们对于百度的btest的了解。在我们之后的测试工作中,可以根据各个项目的特点以及gtest、btest等测试框架的功用,进行协调和因势利导,将更多框架产品用于我们的测试工作中,使我们的测试工作更加正规、更加高效、更加可依赖。由于作者能力有限,文中如果有一些不够清晰不够全面的地方,欢迎指正。
【关键词】gtest, cxxtest, TestSuite, TestCase
1、引言
之所以要向大家介绍测试框架,是因为我们在测试的过程中,会越来越体会到测试规范化与可重用性的重要。一方面,我们要让测试的过程可管理化,要让我们自己、RD、其他同学都能够看懂我们测的是什么,是怎么来测的;另一方面,我们不可能一个项目总是一两个同学一直测下去,如果换到其他同学,需要有一种方式让这种传承更加通用、更加高效。如果能够让一个项目组写测试case使用相同的框架、相同的风格,那么可以避免我们的测试过于山寨,可以让测试的设计和执行更加正规化。
我们选择一些第三方的开源的测试框架也是有原因的:
首先,如果我们的公司内部自己实现了一套测试框架,固然可定制性更高,但是之后就会发觉相当于把测试框架重新做了一遍,业界有很多测试框架,已经并且正在接受全世界程序员的使用、检测和改进,比我们公司内部一个项目的力量要大很多。
其次,正因为这些测试框架是开源的,所以虽然这些框架已经提供了比较完善和简单易用的功能,如果我们需要加上额外的功能,仍然可以在上面进行二次开发,相当于站在了巨人的肩膀上。
现在百度ATD团队也在gtest的基础上发布了btest,可以让gtest的使用更加便捷;btest的运行方式和btest是一致的,如果我们对gtest有更多的了解,那么使用btest也会更加得心应手。下文中的所有的内容都适用于btest。
2、cppunit, cxxtest与gtest框架
在进入百度后,我主要测试的项目是一个后台服务程序,在测试自动化方面做的主要工作是使用shell, python以及c++编写一些客户端、桩模块以及测试驱动程序,来测试这个后台服务程序的各种行为是否符合我们的预期,并且在屏幕中打印出结果:Pass或者Fail. 很幸运的,我后来承担了一个lib库中的一个接口函数的测试,这个接口函数主要是进行URL的解析,虽然这个接口函数的代码量不多,测试工作量也不大,但是却是我之前没有接触过的测试范畴。我接到这个项目时,首先的想法就是这种函数级别的测试非常类似于RD经常要做的单元测试,我可以使用一些单元测试框架来规范我的测试,提高测试的效率,并且使得我的测试case、测试代码等等的复用性更高。
于是我就开始调研使用哪一个测试框架来进行我的测试。与Java语言中的JUnit或者testNG不同,C和C++中的单元测试框架并没有这么普遍的工具,有一点百花齐放的局面。
我首先调研的是cppunit,这是一个与JUnit类似的框架,我在大学时也接触过,但是这个框架很陈旧了,并且有着一些缺点,例如一些类可以消失,一些类名应该修改,一些宏定义应该修改,帮助很少很乱等,这里就不再一一赘述,感兴趣的同学可以百度一下。因为cppunit有着一系列的缺点,cppunit的鼻祖之一重写了一套C/C++单元测试框架,这就是cxxtest。与cppunit相比,可以说cxxtest具有如下一些优点:不需要RTTI(运行时间类型信息);不需要成员模板功能;不需要异常处理;不需要任何外部函数库(包括内存管理、文件/控制台的输入/输出和图形库等);它完全是作为一套头文件的集合而进行发布的。另外由于由于cppunit带有Make文件, 所以只能用在主要的操作系统中,而应用到不常见操作系统中源代码及Make文件修改的工作量就会很大。cxxtest不带Make文件, 所以也可用于其他操作系统中,具有更好的可移植性和可用性。Gtest虽然也使用Makefile文件,但是通过libtool动态加载来实现,可以支持Supports Linux, Windows, Mac OS, 以及其他操作系统。
当然cxxtest也有一些缺点,例如需要用到perl或者python对测试代码的头文件进行文法扫描,生成可执行代码,准备工作比较麻烦。并且cxxtest的绝大多数功能都可以使用gtest来完成,所以一般情况下就更加推荐gtest,一步到位。在百度ATP的自动化测试工具推荐名单中,gtest也是作为一个推荐的工具来给出的,并且指出了cxxtest的使用可以转为用gtest代替。下面先给大家列出一些cxxtest和gtest之间的比较,这样大家就有个直观的印象。
图1 gtest和cxxtest的比较
在这次lib库的测试中,因为com组的qa同事使用的是cxxtest框架,所以我也使用了cxxtest框架,但是根据从网上了解的gtest的种种优点,我也学习了gtest的使用,在这里就主要向大家推荐gtest的使用,这样更能一步到位,免得总是不停地更换测试框架。
首先介绍一下gtest测试框架。这是Google的开源C++单元测试框架,是遵循 New BSD License (可用作商业用途)的开源项目。据说google内部的大多数C++代码都已经使用这个测试框架进行单测,gtest 可以支持绝大多数大家所熟知的平台。Gtest的使用较为方便,与 CppUnit 不同的是,gtest 可以自动记录下所有定义好的测试,不需要用户通过列举来指明哪些测试需要运行。
要了解gtest测试框架,一个很方便的学习资源就是gtest的官方网站 http://code.google.com/p/googletest/ . 在这个网址,读者也可以下载到gtest的源代码,用于阅读学习乃至在上面的改进。在本文中,我先根据自己的使用经验,给大家做一些介绍。
3、gtest 简单示例
我们就开始着手试一下gtest的基本功能吧,首先从http://code.google.com/p/googletest/ 这个网站下载下gtest的压缩包,解压到Linux环境的测试机器。然后我们进入解压后的gtest目录,首先 ./configure 然后 make ,然后我们就可以使用这个gtest测试框架了。
我们写一段简单的例子,try.cpp:
//try.cpp
#include "gtest/gtest.h"
namespace
{
// The fixture for testing class Foo.
class FooTest : public ::testing::Test
{
protected:
FooTest()
{
// You can do set-up work for each test here.
}
virtual ~FooTest()
{
// You can do clean-up work that doesn't throw exceptions here.
}
virtual void SetUp()
{
// Code here will be called immediately after the constructor (right
// before each test).
}
virtual void TearDown()
{
// Code here will be called immediately after each test (right
// before the destructor).
}
// Objects declared here can be used by all tests in the test case for Foo.
};
// Tests that the Foo::Bar() method does Abc.
TEST_F(FooTest, ZeroEqual)
{
EXPECT_EQ(0,0);
}
// Tests that Foo HiHiHi.
TEST_F(FooTest, OneEqual)
{
EXPECT_EQ(1,1);
}
} // namespace
上面这段代码就是一个很简单的gtest调用代码。我们需要按照下面的方面来对这些代码进行编译。因为gtest库提供的是.la文件,所以我们需要使用libtool来进行编译。
首先我们将try.cpp编译成.o文件,这时候需要用到gtest的include头文件。
g++ -o try.o -c try.cpp -I/home/work/gtest/gtest-1.4.0/include
然后我们使用libtool来生成可以运行的gtest测试程序。
libtool --mode=link gcc -g -O -o try try.o /home/work/gtest/gtest-1.4.0/lib/libgtest_main.la –lm
从上面这条命令,我们可以看出,gtest提供了一个可以直接使用的libgtest_main.la,这样我们不需要编写main文件就可以生成可执行的程序try. 其实gtest为我们写好的main文件也非常简单,内容如下:
int _tmain(int argc, _TCHAR* argv[])
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
其中的RUN_ALL_TESTS()就是直接运行所有的测试case。
如果我们需要在main函数中添加额外的内容,也可以自己来写main文件,也非常简单。
经过上面的步骤之后,我们的目录中,有了可执行文件try,我们运行./try之后,可以看到如下内容:
4、gtest在das和kr项目中的应用
虽然gtest主要是一个单元测试的框架,但是我们却不一定只能在单元测试的时候用到它。其实只要我们原因,在我们的每个测试case外边加一个包裹函数wrapper,然后针对这些包裹函数进行gtest测试即可。
这样做的好处也很直观,就是将所有的自动化case都尽量统一形式,便于回归和管理。例如我们要测一个网络服务的功能,向服务器发送“A”,服务器返回“B”。这时候就可以将这个客户端发送和接收请求的过程包裹进gtest的testcase中,并且对返回结果进行EXPECT_EQ判断是否与预期结果相等,就可以了。
可能在第一次写测试case的时候会多一些代码,但是在case多了的时候,gtest详细好用的输出提示会让我们在批量运行case的时候受益良多。
在我测过的项目中,das和kr都在一定程度上用到了gtest的各种功能来提高测试效率。
在das-comm项目中,case的组织结构如下图所示:
图2 das中gtest组织结构
上图中从坐到右分别是基于gtest的case引擎,case的数据的物理组织,和各个case共用的一个engine工具。所有的case都被组织进一套gtest的Test Suite里,而每个case所需要的数据是按照E家宝上的目录结构进行组织的,每个case的物理目录会作为参数传入gtest。
在上图的gtest的case引擎里,针对das-comm项目,我们写了一个函数int checkFunc(string casePath) ,参数就是刚刚传入的路径。这个函数的功能是从路径中载入这个case的数据,调用工具,比对结果。之后每个case的代码都是基于这个函数的,会根据这个函数的返回值来判断某一项功能测试是否取得了预期效果。
总结一下,就是尽量将各个case的公有的部分提取到一起进行复用,同时利用gtest提供的丰富的验证函数来达到我们的测试的目的。
运行gtest之后的效果如下:
而在kr项目中,我们进一步使用到本文中将介绍的批量测试中的参数传递来减轻我们的工作量。
我们发现很多test case之间需要变动的往往只有一两个参数,所以我们把这部分生成参数的工作集中到了一起,放进一个参数生成器,然后将各个test case中公共的地方提取出来,直接从这个参数生成器直接读入参数即可。幸运的是,现在的gtest对于这样的需求已经做到了较好的支持。
我们首先引用相关的类:
using ::testing::TestWithParam;
using ::testing::Values;
class lcTest : public TestWithParam<int>
然后gtest里面的这个宏就是我们需要的参数生成器:
INSTANTIATE_TEST_CASE_P(
ParamUsedInTest, lcTest, Values(1,10,100,1000,1000) );
在test case里面,我们只需要使用GetParam() 就可以:
TEST_P(lcTest, PVLimit_1)
{
ASSERT_PRED1( checkFunc, GetParam());
}
通过这种方法,我们在编写各个test case的时候,就减少了出错的概率,也降低了工作量。这种批量传输参数的方法在后面还会有更加详细的介绍。
5、gtest测试框架结构
从前面的gtest的简单示例,我们对使用gtest进行测试的基本流程有了大概的了解。但是我们还是要进一步了解一下gtest测试框架的层次结构。简单地讲,每个基于gtest的测试过程,是可以分为多个TestSuite级别,而每个TestSuite级别又可以分为多个TestCae级别。这样分层的结构的好处,是可以针对不同的TestSuite级别或者TestCae级别设置不同的参数、事件机制等,并且可以与实际测试的各个模块层级相互对应,便于管理。
在这里,首先介绍一下gtest提供的多种事件机制,这些事件机制方便了我们在案例之前或之后做一些操作。gtest的事件一共有3种:1. 全局的,所有案例执行的前后。2. TestSuite级别的,在某一批TestCase中第一个TestCase前,最后一个TestCase执行后。3. TestCase级别的,每个TestCase前后。
首先介绍全局事件。实现全局事件的方法是写一个类,继承testing::Environment类,实现里面的SetUp和TearDown方法。这其中,SetUp()方法会在所有案例执行前执行,而TearDown()方法会在所有案例执行后执行。例如我们在前面的try.cpp中加上一些内容:
//try.cpp
#include "gtest/gtest.h"
namespace
{
class GrobalEvent : public testing::Environment
{
public:
virtual void SetUp()
{
std::cout << "gtest introduction example. SetUp." << std::endl;
}
virtual void TearDown()
{
std::cout << "gtest introduction example.TearDown." << std::endl;
}
};
class FooTest : public ::testing::Test
{
};
TEST_F(FooTest, ZeroEqual)
{
EXPECT_EQ(0,0);
}
TEST_F(FooTest, OneEqual)
{
EXPECT_EQ(1,1);
}
在定制好这个类之后,我们需要在main函数中使用testing::AddGlobalTestEnvironment方法将这个类中的事件添加进来,也就是说在一次测试中我们可以添加多个这样的事件,至于事件的执行顺序,同学们有兴趣的话可以自己尝试一下。
int _tmain(int argc, _TCHAR* argv[])
{
testing::AddGlobalTestEnvironment(new GrobalEvent);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
对于TestSuite的事件,我们需要写一个类,继承testing::Test,然后实现两个静态方法:
1. SetUpTestCase() 方法在第一个TestCase之前执行
2. TearDownTestCase() 方法在最后一个TestCase之后执行
我们同样在try.cpp中进行添加:
//try.cpp
#include "gtest/gtest.h"
namespace
{
// The fixture for testing class Foo.
class FooTest : public ::testing::Test
{
protected:
static void SetUpTestCase()
{
std::cout << " SetUpTestCase." << std::endl;
}
static void TearDownTestCase()
{
std::cout << " TearDownTestCase." << std::endl;
}
};
TEST_F(FooTest, ZeroEqual)
{
EXPECT_EQ(0,0);
}
TEST_F(FooTest, OneEqual)
{
EXPECT_EQ(1,1);
}
在这里,我们就注意到了,后面的TEST_F所使用的参数的第一个都是这个类的名字,其实这就是这个TestSuite的名字,TEST_F的第一个参数需要是TestSuite。
最后,是TestCase的事件,这种事件是挂在每个案例执行前后的,实现方式和上面的几乎一样,不过需要实现的是SetUp方法和TearDown方法:1. SetUp()方法在每个TestCase之前执行;2. TearDown()方法在每个TestCase之后执行。其实这两个函数已经在上面多次出现了,我们进行一些修改:
//try.cpp
#include "gtest/gtest.h"
namespace
{
class FooTest : public ::testing::Test
{
protected:
virtual void SetUp()
{
std::cout << " testcase event setup." << std::endl;
}
virtual void TearDown()
{
std::cout << " testcase event teardown." << std::endl;
}
};
TEST_F(FooTest, ZeroEqual)
{
EXPECT_EQ(0,0);
}
TEST_F(FooTest, OneEqual)
{
EXPECT_EQ(1,1);
}
} //namespace
我们编译程序后,运行./try,可以看到如下结果,说明在最后一种场景中,每个case中都运行了一遍初始化和清理工作,符合我们的预期:
6、gtest的各种测试函数
这一部分主要总结gtest中的各种测试函数,也可以称为断言。 gtest中,测试函数可以理解为分为两类,一类是ASSERT系列,一类是EXPECT系列。一个直观的区别是:
1. ASSERT_* 系列的断言,当检查点失败时,退出当前函数(注意:并非退出当前案例)。
2. EXPECT_* 系列的断言,当检查点失败时,继续往下执行。
先介绍一个简单的断言,EXPECT_EQ,我们如果这样来写EXPECT_EQ(1,2),那么执行测试之后会有如下的输出:
这些输出也可以重定向到XML文件中,方便进行统一格式的处理和分析。
我们如果需要输出额外的信息,可以直接使用<<操作符,例如:EXPECT_EQ(x[i], y[i]) << “Extra output”,有时候这些额外的输出也很有用。
下面还有一些其他常见的断言,我们在gtest的网站上大概看过一遍,应该就有了大概的了解,名字都很直观。
布尔值检查:
Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false
数值型数据检查:
Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(expected, actual);EXPECT_EQ(expected, actual);expected == actual
ASSERT_NE(val1, val2); EXPECT_NE(val1, val2); val1 != val2
ASSERT_LT(val1, val2); EXPECT_LT(val1, val2); val1 < val2
ASSERT_LE(val1, val2); EXPECT_LE(val1, val2); val1 <= val2
ASSERT_GT(val1, val2); EXPECT_GT(val1, val2); val1 > val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2
字符串检查:
Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(expected_str, actual_str); EXPECT_STREQ(expected_str, actual_str); the two C strings have the same content
ASSERT_STRNE(str1, str2); EXPECT_STRNE(str1, str2); the two C strings have different content
ASSERT_STRCASEEQ(expected_str, actual_str);EXPECT_STRCASEEQ(expected_str, actual_str); the two C strings have the same content, ignoring case
ASSERT_STRCASENE(str1, str2);EXPECT_STRCASENE(str1, str2); the two C strings have different content, ignoring case
除了上面这些断言,还有断言可以直接返回成功或者直接返回失败:
Fatal assertion Nonfatal assertion
FAIL(); ADD_FAILURE();
使用时也很方便:
TEST(ExplicitTest, Demo)
{
ADD_FAILURE() << “Fail, but continue.”;
FAIL(); <<”Fail, and return.”
}
Gtest还可以检查程序抛出的异常,可以使用下面这些断言:
Fatal assertion Nonfatal assertion Verifies
ASSERT_THROW(statement, exception_type); EXPECT_THROW(statement, exception_type); statement throws an exception of the given type
ASSERT_ANY_THROW(statement); EXPECT_ANY_THROW(statement); statement throws an exception of any type
ASSERT_NO_THROW(statement); EXPECT_NO_THROW(statement); statement doesn't throw any exception
对于返回bool值的函数,还可以直接在断言中传入参数。这是因为程序员在使用EXPECT_TRUE或ASSERT_TRUE时,经常希望能够输出更加详细的信息,比如检查一个函数的返回值TRUE还是FALSE时,希望能够输出传入的参数是什么,以便失败后好跟踪。因此提供了如下的断言:
Fatal assertion Nonfatal assertion Verifies
ASSERT_PRED1(pred1, val1); EXPECT_PRED1(pred1, val1); pred1(val1) returns true
ASSERT_PRED2(pred2, val1, val2); EXPECT_PRED2(pred2, val1, val2); pred2(val1, val2) returns true
这些断言最多支持5个参数的输入,使用方法如下:
bool func(int m, int n)
{
return func2(m , n) > 1;
}
TEST(SelfDefineTest, BoolTest)
{
int m = 1, n = 2;
EXPECT_PRED2( func, m, n);
}
当成功时,会输出:
失败时,会输出:
有时候,我们想要自动义一些输出,并且这些输出又需要与各个函数的输入有关,我们就可以使用下面的断言:
Fatal assertion Nonfatal assertion Verifies
ASSERT_PRED_FORMAT1(pred_format1, val1);` EXPECT_PRED_FORMAT1(pred_format1, val1); pred_format1(val1) is successful
ASSERT_PRED_FORMAT2(pred_format2, val1, val2); EXPECT_PRED_FORMAT2(pred_format2, val1, val2); pred_format2(val1, val2) is successful
具体的使用方法可以查看gtest的帮助手册。
另外,浮点数的检查需要与整数的检查区别对待。
对“相同”的浮点数的比较:
Fatal assertion Nonfatal assertion Verifies
ASSERT_FLOAT_EQ(expected, actual); EXPECT_FLOAT_EQ(expected, actual); the two float values are almost equal
ASSERT_DOUBLE_EQ(expected, actual); EXPECT_DOUBLE_EQ(expected, actual); the two double values are almost equal
对相近的浮点数的比较:
Fatal assertion Nonfatal assertion Verifies
ASSERT_NEAR(val1, val2, abs_error); EXPECT_NEAR(val1, val2, abs_error); the difference between val1 and val2 doesn't exceed the given absolute error
其他还有一些断言,感兴趣的同学可以到官方网站上去了解,这里就不再一一赘述了。
7、gtest批量测试中的参数传递
有时候,我们针对一个函数,需要使用不同的输入输出组合来进行测试。我们在本文开始部分介绍的kr项目的测试case,就是这样的例子。这时候很容易出现代码重复的情况,而我们知道软件中的很多隐患都是由于拷贝粘贴引起的,所以我们要尽量避免代码复制。在一般的测试框架中,很容易就会出现这样的代码:
TEST(myTest, aLotTest)
{
EXPECT_TRUE(func(1));
EXPECT_TRUE(func(10));
EXPECT_TRUE(func(100));
EXPECT_TRUE(func(1000));
EXPECT_TRUE(func(10000)));
}
在gtest中,就可以避免这种情况的产生。我们需要首先定义一个类,继承testing::TestWithParam<T>,其中T就是需要参数化的参数类型,比如对于前面的代码我们需要参数化一个int型的参数。
class myTest : public::testing::TestWithParam<int>
{
};
然后,我们需要使用到TEST_P这个宏,这个P就可以被理解为parameterized. 在TEST_P宏里,我们就可以使用GetParam()来获取当前的参数的具体值,如下所示:
TEST_P(myTest, aLotTest)
{
int n = GetParam();
EXPECT_TRUE(func (n));
}
最后,我们使用宏INSTANTIATE_TEST_CASE_P来告诉gtest这个函数的参数范围:
INSTANTIATE_TEST_CASE_P(PreName, myTest, testing::Values(1, 10, 100, 1000, 10000));
第一个参数是测试结束时进行打印的一系列测试案例的前缀,无限制。
第二个参数是测试case的名称,需要和之前定义的参数化的类的名称相同,在这个例子里是myTest.
第三个参数就是参数生成器。除了上面的例子所使用的test::Values,gtest还提供了一系列的参数生成的函数:
Range(begin, end[, step]) 范围在begin~end之间,步长为step,不包括end
Values(v1, v2, ..., vN) v1,v2到vN的值
ValuesIn(container) and ValuesIn(begin, end) 从一个C类型的数组或是STL容器,或是迭代器中取值
Bool() 取false 和 true 两个值
Combine(g1, g2, ..., gN) 这个比较强悍,它将g1,g2,...gN进行排列组合,g1,g2,...gN本身是一系列参数生成器,每次分别从g1,g2,..gN中各取出一个值,组合成一个元组(Tuple)作为一个参数。
说明:这个功能只在提供了<tr1/tuple>头的系统中有效。gtest会自动去判断是否支持tr/tuple,如果你的系统确实支持,而gtest判断错误的话,你可以重新定义宏GTEST_HAS_TR1_TUPLE=1。
通过gtest的这种机制,我们就可以提高测试case的代码质量,减少重复的代码和工作量,提高效率。
8、结束语
本文中的内容,一定程度上是想起到一种抛砖引玉的作用,或者说希望触发大家对gtest以及百度按照自身需求定制的btest的兴趣,从而推进我们测试工作的流程化,让更多更好的成熟的测试框架在我们团队中得到使用。
本文中的案例和内容是从笔者的使用经验中获得,但是由于笔者能力有限,所以还希望大家多多指出错误、意见和建议。也希望同学们能够更多地了解业界更新更好的测试工具、测试框架、测试方法等,并且在团队内分享,来促进我们测试工作的发展。欢迎同学们就文章中的内容与我进一步交流,谢谢!
9、参考文献
1.Gtest主页:
(全文完)
【本文转自