一、简介
使用Gtest基本上能够满足绝大多数的测试场景,但是对于一些涉及多个模块交互、文件系统、网络通信等复杂的场景,很难仿真一个真实的环境进行单元测试。这时就需要引入模拟对象Mock Objects来模拟程序的一部分来构造测试场景。
Google C++ Mocking框架(简称Gmock)是一个库,可以用来创建模拟类并使用它们。通过定义模拟对象来模拟这些类的行为操作,通过Gmock提供的接口,构造自己想要的返回值和行为,模拟出一切你想要在测试中需要的东西。
Gmock提供了以下特性:一是轻松地创建mock类;二是支持丰富的匹配器(Matcher)和行为(Action);三是支持丰富的期望行为的定义;四是多平台的支持。
一般的,将Gmock与Gtest搭配使用。
二、Gmock使用方法
Gmock的主要的思想是通过mock类模拟出当前已实现的功能或者未实现的功能。未实现的功能比较简单,这里不在展开,接下来主要说一下如何模拟一个已实现的功能。
2.1 基本步骤
1、使用一些简单的宏描述你想要模拟的接口,将其扩展到你的mock类的实现上;
2、创建一些模拟对象,并设置其期望和行为;
3、使用模拟对象的代码进行测试;
2.2 准备工作
准备工作主要是定义mock类以及一些宏等资源,方便后面设置模拟方法的各种选项,这里举一个简单的例子方便理解:
#include <gtest/gtest.h>
#include <gmock/gmock.h>class MockConn{
public:MOCK_METHOD2(mock_func1, int(int, char*));MOCK_METHOD0(mock_func2, char*(void));MOCK_METHOD1(mock_func3, void (float));......
}MockConn *mock = NULL;#define func1(try, ip) mock_obj->moc_func1(trx, ip)#include “path/to/file.cc”#undef func1Class MockTest:public::testing::Test{......
}
首先头文件包含同Gtest,接着定义一个mock类,mock类中要描述出你想要模拟的接口。
假如现在需要测试函数的原型为int func1(int try, char* ip);,这个函数的功能是对一个ip地址设置它的最大尝试连接次数,函数返回实际的连接的次数。
MOCK_METHOD*(mock_func_name, prototype)宏用于描述你要模拟的方法的特性,MOCK_METHOD后面的数字*表示传入参数的个数,mock_func_name表示将要用此宏来替换想要的模拟的方法,prototype为函数的原型,包括返回值的类型和传入参数的类型,不写函数名,形参名可写可不写。
接下来定义全局模拟方法对象的指针mock,然后定义宏替换mock类中的方法,这一步旨在用宏替换真正已实现的func1函数,模拟func1的返回值。后面通过#include将真正func1的实现覆盖成模拟方法mock_func1,接下来通过#undef解除宏定义,这样就可以给模拟方法设置期望行为了。
2.3 设置方法的期望行为
在Gmock中,使用EXPECT_CALL()宏来设置模拟方法的各项期望值,一般的格式为:
EXPECT_CALL(mock_object, method(matchers)).Times(cardinality).WillOnce(action).WillRepeatedly(action);
这个宏有两个参数:首先是mock对象,然后是方法及其参数。后面的参数可选,.Times(cardinality)设置预期执行的次数,.WillOnce(action)设置一次执行的行为, .WillRepeatedly(action)设置缺省/重复执行的行为。当然还可以设置其他的选项,下面重点介绍一下。
2.3.1 匹配器(Matcher)
匹配器(Matcher)用于定义mock类中的方法的形参的值,检查传入参数是否符合预期设置的条件。支持各种类型的比较,比如整数比较、浮点数比较、字符串比较、容器比较等,这些匹配符都在Gmock的*::testing*这个命名空间下,使用时需要先引入这个名空间。这里列出常用的一些匹配符。
一般比较
Eq(value) 或者 value | argument == value |
Ge(value) | argument >= value |
Gt(value) | argument > value |
Le(value) | argument <= value |
Lt(value) | argument < value |
Ne(value) | argument != value |
IsNull() | method的形参必须是NULL指针 |
NotNull() | argument is a non-null pointer |
Ref(variable) | 形参是variable的引用 |
TypedEq(value) | 形参的类型必须是type类型,而且值必须是value |
A() or An() | 任意值 |
浮点数的比较
DoubleEq(a_double) | 形参是一个double类型,形参值近似于a_double |
FloatEq(a_float) | 同上,只不过类型是float |
NanSensitiveDoubleEq(a_double) | 形参是一个double类型,形参值近似于a_double, |
NanSensitiveFloatEq(a_float) | 同上,只不过形参是float |
字符串匹配
这里的字符串即可以是C风格的字符串,也可以是C++风格的。
ContainsRegex(string) | 形参匹配给定的正则表达式 |
EndsWith(suffix) | 形参以suffix截尾 |
HasSubstr(string) | 形参有string这个子串 |
MatchesRegex(string) | 从第一个字符到最后一个字符都完全匹配给定的正则表达式. |
StartsWith(prefix) | 形参以prefix开始 |
StrCaseEq(string) | 参数等于string,并且忽略大小写 |
StrCaseNe(string) | 参数不是string,并且忽略大小写 |
StrEq(string) | 参数等于string |
StrNe(string) | 参数不等于string |
容器的匹配
很多STL的容器的比较都支持==这样的操作,对于这样的容器可以使用上述的ContainerEq(container)来比较。也可以使用下面的这些容器匹配方法:
Contains(e) | 在method的形参中,只要有其中一个元素等于e |
Each(e) | 参数各个元素都等于e |
ElementsAre(e0, e1, …, en) | 形参有n+1的元素,并且一一匹配 |
ElementsAreArray(array) 或者ElementsAreArray(array, count) | 和ElementsAre()类似,除了预期值/匹配器来源于一个C风格数组 |
ContainerEq(container) | 类型Eq(container),就是输出结果有点不一样,这里输出结果会带上哪些个元素不被包含在另一个容器中 |
2.3.2 基数(Cardinalities)
基数(Cardinalities)用于Times()中来指定模拟函数将被调用多少次|,一般它的值设置为一个具体的数,有时可能设置在一个区间,这里介绍几个较为常用的用法:
AnyNumber() | 函数可以被调用任意次. |
AtLeast(n) | 预计至少调用n次. |
AtMost(n) | 预计至多调用n次. |
Between(m, n) | 预计调用次数在m和n(包括n)之间. |
Exactly(n) 或 n | 预计精确调用n次. 特别是, 当n为0时,函数应该永远不被调用. |
2.3.3 行为(Actions)
行为(Actions)用于指定mock类的方法所期望模拟的行为:比如返回什么样的值,对引用、指针赋上什么值,等等。 值的返回类型常用的有:
Return() | 让Mock方法返回一个void结果 |
Return(value) | 返回值value |
ReturnNull() | 返回一个NULL指针 |
ReturnRef(variable) | 返回variable的引用. |
ReturnPointee(ptr) | 返回一个指向ptr的指针 |
另一面的作用(Side Effects)
Assign(&variable, value) | 将value分配给variable |
SetArrayArgument(values, values+num) | 设置内部中间变量的值 |
使用函数或者函数对象(Functor)作为行为
Invoke(f) | 使用模拟函数的参数调用f, 这里的f可以是全局/静态函数或函数对象. |
Invoke(object_pointer, &class::method) | 使用模拟函数的参数调用object_pointer对象的mothod方法. |
复合动作
DoAll(a1, a2, …, an) | 每次发动时执行a1到an的所有动作. |
IgnoreResult(a) | 执行动作a并忽略它的返回值. a不能返回void. |
2.3.4 序列(Sequences)
默认定义的期望行为是无序(Unordered)的,但有时候需要定义有序的(Ordered)的调用方式,即序列 (Sequences) 指定预期的顺序。在同一序列里的所有预期调用必须按它们指定的顺序发生;反之则可以是任意顺序。
Sequence s1, s2;
EXPECT_CALL(mockFoo, getSize()).InSequence(s1, s2).WillOnce(Return(1));
三、编译运行
MySQL源码下的Unittest默认是不运行的,这是因为MySQL默认不带Gtest,因此想要编译运行,需要添加参数:
一种方法是在线自动安装。在cmake的时候加 -DENABLE_DOWNLOADS=1参数,它会自动从Github下载对应版本的googletest。这是比较推荐的方法。
另一种方法是通过本地zip包安装。一般5.7.x需要的版本是release-1.8.0,如果不确定可以查看unittest/gunit/CMakeLists.txt需要的版本。然后cmake 时加-DLOCAL_GMOCK_ZIP=/path/to/gooletest-release-1.8.0.zip ,路径改为自己的路径。
Unittest提供整合编译和单独编译的两种方式。方法为在cmake时添加-DMERGE_UNITTESTS=1,1为整合编译,0为单独编译。
图1 MySQL Test Unittest测试用例执行成功的结果图
图1 MySQL Unittest测试用例执行失败的结果图
四、结束语
代码的正确性是一个程序的首要前提,会编写单元测试是每一个开发者应具备的基本技能。Gtest和Gmock是非常棒的C++单测框架,学习并灵活应用这些单测工具,可以提升软件的稳定性和健壮性,方便定位问题修复缺陷,更高效率得进行程序功能的扩展和项目代码的维护。