引入
Google test是一种比较方便的C++测试框架,它可以帮助我们比较方便的进行测试代码,以及输出尽可能详细的失败信息,能够大大缩短我们测试代码的编写效率,而且这个框架的使用也比较简单。
之前还在学校学习过Junit框架,作为一个java程序员或多或少接触到这个框架,同样的C++的测试框架最常用的就是GTest。
什么是断言?
上述两个测试框架都是断言式测试框架,了解测试框架首先就要了解什么是断言。
在平时的开发当中,一个项目往往包含了大量的方法,可能有成千上万个。如何去保证这些方法产生的结果是我们想要的呢?当然了,最容易想到的一个方式,就是我们通过System.out来输出我们的结果,看看是不是满足我们的需求,但是项目中这些成千上万个方法,我们总不能在每一个方法中都去输出一遍。而且对于测试人员来说这个函数我也不需要知道细节,我们只需要知道这个函数需要什么参数和返回什么结果。
在这个环境下包裹式的断言式框架就应运而生,断言就是一种在程序中的一阶逻辑,当程序执行到断言的位置时,对应的断言就应该为真,若断言不为真时,程序会中止运行,并给出错误消息。
这样看它和if逻辑很像,两者 的区别就是断言语句只会在debug版本中才有效是用来调试和定位错误的,而if是正常程序逻辑的一部分。
为什么学习GTest框架?
当我们把一个函数用断言函数包裹时就构成了一个测试用例,我们甚至可以自己规定自己的断言函数而写一个自己的框架。
我们之所以学习框架就是它足够的完善,GoogleTest采用了一系列断言来进行代码测试,定义了许多宏,当断言失败时Google Test将会打印出assertion时的源文件和出错行的位置,以及附加的失败信息,
用户可以直接通过“<<”在这些断言宏后面跟上自己希望在断言命中时的输出信息。
测试用例
我们直接通过一个简单的测试用例来看这个框架的语法(语法就是C++的语法,其实就是用框架中定义的一些函数将你要测试的函数或类方法包裹起来用于生成传入参数和验证输出结果。)
#include "log.h"
#include "gtest/gtest.h"
void ThrowException(int n) {
switch (n) {
case 0:
throw 0;
case 1:
throw "const char*";
case 2:
throw 1.1f;
case 3:
return;
}
}
TEST(ThrowException, Check) {
EXPECT_THROW(ThrowException(0), int);
EXPECT_THROW(ThrowException(1), const char*);
ASSERT_ANY_THROW(ThrowException(2));
ASSERT_NO_THROW(ThrowException(3));
}
这个TEST测试函数就是我们预期ThrowException在传入0时,会返回int型异常;传入1时,会返回const char*异常。传入2时,会返回异常,但是异常类型我们并不关心。传入3时,不返回任何异常。当然ThrowExeception的实现也是按以上预期设计的。
这个很像我们写一个函数的时候,像测试这个函数的功能的时候就会把它从项目中抽离出来,用一个main函数去调用它的感觉;
这个测试函数做的工作也差不多,区别就是你不需要再把这个方法抽离项目了,单独再写一个测试类就可以做这个测试了,而且项目上线后这个测试函数会默认不再运行。
框架的一些常用宏和函数
框架测试宏
测试宏可以分为两大类:
这些成对的断言功能相同,但效果不同。
其中ASSERT_*将会在失败时产生致命错误并中止当前调用它的函数执行(注意不是当前测试用例)。
而EXPECT_会生成非致命错误,不会中止当前函数,而是继续执行当前函数。通常情况应该首选使用EXPECT_,因为ASSERT_*在报告完错误后不会进行清理工作,有可能导致内容泄露问题。
基本断言(真值比较)
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(val1,val2); |
EXPECT_EQ(val1,val2); |
val1 == val2 |
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(str1,str2); |
EXPECT_STREQ(str1,str2); |
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(str1,str2); |
EXPECT_STRCASEEQ(str1,str2); |
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 |
浮点对比断言
在对比数据方面,我们往往会讨论到浮点数的对比。因为在一些情况下,浮点数的计算精度将影响对比结果,所以这块都会单独拿出来说。GTest对于浮点数的对比也是单独的
Fatal assertion |
Nonfatal assertion |
Verifies |
ASSERT_FLOAT_EQ(val1, val2); |
EXPECT_FLOAT_EQ(val1, val2); |
the two float values are almost equal |
ASSERT_DOUBLE_EQ(val1, val2); |
EXPECT_DOUBLE_EQ(val1, val2); |
the two double values are almost equal |
almost euqal表示两个数只是近似相似,默认的是是指两者的差值在4ULP之内(Units in the Last Place)。我们还可以自己制定精度
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 |
成功失败断言
该类断言用于直接标记是否成功或者失败。可以使用SUCCEED()宏标记成功,使用FAIL()宏标记致命错误(同ASSERT_),ADD_FAILURE()宏标记非致命错误(同EXPECT_)
if (Check) {
SUCCEED();
}
else {
FAIL();
}
这儿有个地方需要说一下,SUCCEED()宏会调用GTEST_MESSAGE_AT_宏,从而会影响TestResult的test_part_results结构体,这也是唯一的成功情况下影响该结构体的地方。
异常断言
异常断言是在断言中接收一定类型的异常,并转换成断言形式。
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 |
参数名输出断言
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 |
template <typename T1, typename T2>
bool GreaterThan(T1 x1, T2 x2) {
return x1 > x2;
}
TEST(PredicateAssertionTest, AcceptsTemplateFunction) {
int a = 5;
int b = 6;
ASSERT_PRED2((GreaterThan<int, int>), a, b);
}
宏
看上面的用例大家也看出来了,所有的测试代码都被一个TEST所包裹起来了,这不是一个函数而是一个宏,宏就是用来封装一个测试代码块的一种定义字。
(额外说一句,Java中的Junit测试用例是用的注解机制,C++的GTest使用的宏定义机制,其本质是一样的,都是为了表示和区分测试代码和程序逻辑代码)
除了上面写到的TEST宏,这个框架还有TEST_F宏、TEST_P宏等下面就分别介绍几者的区别和用处。
测试用例(Test Case)是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求,测试特例是测试用例下的一个(组)测试;
我们要写的测试代码都会包含在一个宏中构成一个测试特例。
TEST宏
TEST宏是一个很重要的宏,它构成一个测试特例。
TEST宏的第一个参数是test_case_name(测试用例名),第二个参数是test_name(测试特例名)。
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));
}
对于我们定义的测试用例名和测试特例名,不能有下划线(_)。因为GTest源码中需要使用下划线把它们连接成一个独立的类名
也不能有相同的“测试用例名和特例名”的组合——否则类名重合。
测试用例名和测试特例名的分开,使得我们编写的测试代码有着更加清晰的结构——即有相关性也有独立性。相关性是通过相同的测试用例名联系的,而独立性通过不同的测试特例名体现的。
TEST_F宏
场景:我们要测试向数据库插入(id,name,location)这样的三个数据,那要先构建一个基础数据(0,Fang,Beijing)。我们第一个测试特例可能需要关注于id这个字段,于是它要在基础数据上做出修改,将(1,Fang,Beijing)插入数据库。第二个测试特例可能需要关注于name字段,于是它要在基础数据上做出修改,将(0,Wang,Beijing)插入数据库。第三个测试特例可能需要关注于location字段,于是它要修改基础数据,将(0,Fang,Nanjing)插入数据库。如果使用GTEST宏来测试的话,那么每个测试特例前,我们需要把所有的数据填充好,再去操作。真实场景中一条记录往往不止三个数据,这样做会显得非常繁琐和不直观。
Google工程师早就考虑到这样的场景,可以将上述的场景提炼一下,其实我们只要在每个特例执行前,获取一份基础数据(原始数据),然后修改其中本次测试特例关心的一项就可以了。同时这份基础数据不可以在每个测试特例中被修改——即本次测试特例获取的基础数据不会受之前测试特例对基础数据修改而影响——获取的是一个恒定的数据。
这个时候我们就需要使用TEST_F宏了,TEST_F叫作测试套件。
我们直接看一个例子来理解:
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);
}
这相当于我们使用一个TestFixtures类继承于::testing::Test类,将需要改变的数值封装起来,数据改变的操作就不用再设置很多参数了,而是通过同一个类保护这个基础数据。
像上述代码,First测试特例中,我们修改了data的数据(23行),第24行验证了修改的有效性和正确性。在second的测试特例中,一开始就检测了data数据(第28行),如果First特例中修改data(23行)影响了基础数据,则本次检测将失败。我们将First和Second测试特例的实现定义成一样的逻辑,可以避免编译器造成的执行顺序不确定从而影响测试结果。
TEST_P宏
这个宏和TEST_F大致相同,第一个参数是一个已定义类名,第二个参数是测试特例名,都是为了多场景下的测试,每个场景都可能要细致地考虑到到各个参数的选择时框架就提供了一种宏帮助我们组合场景和参数,它就是TEST_P宏。
它的TEST_F的区别是TestFixtures这个测试类我们不是继承的
:
:
t
e
s
t
i
n
g
:
:
T
e
s
t
::testing::Test
::testing::Test,而是继承的
:
:
t
e
s
t
i
n
g
:
:
W
i
t
h
P
a
r
a
m
I
n
t
e
r
f
a
c
e
<
T
>
::testing::WithParamInterface< T>
::testing::WithParamInterface<T> ,这样我们可以使用重写这个类中的GetPara方法拿到参数的具体值,通过不同的参数改变而直接改变场景中的组合方式。