如何解决虚函数对象依赖?
在面向对象的开发过程中,由于需要将各种属性或者事物按一定的规律抽象为独立的一个对象,然后按需进行调用,如此一来,对象之间的依赖便无可避免,设计不好更会产生双向依赖、交叉依赖等困境,那么我们在面对这种对象间依赖的情况下,该如何进行单元测试呢?
设想一个场景:如果我们正在开发的模块有个处理,需要先向后台请求一些数据,然后再根据后台响应的数据,进行一定的处理,
但是,我们后台还没开发完成,预计还需要几个月才能搭起一个可连接的环境,我们总不能等到几个月后才开展工作吧?
面对上述类似的这种情况,我们可以在单元测试中引入一个mock
对象,由其来模拟一些未完成的接口操作、未实现的后端请求接口操作、未实现的对象接口等,通过设计对应的输入及预期中对应的输出,那么我们可以在开发阶段,绕开对实际对象/请求的依赖,完成我们程序功能的开发。
例如,我们有一个网络类
头文件:
// head
class simulateSocket
{
public:
simulateSocket(){}
virtual ~simulateSocket(){}
virtual int send(unsigned char* buff) = 0;
virtual void recv(unsigned char* buff, int len) = 0;
};
class network: public simulateSocket
{
private:
public:
network();
~network();
virtual int send(unsigned char* buff);
virtual void recv(unsigned char* buff, int len);
int requestUrl(string url, unsigned char* data);
};
源文件:
// source
int network::send(unsigned char* buff)
{
return 0;
}
void network::recv(unsigned char* buff, int& len)
{
len = 0;
}
int network::requestUrl(string url, unsigned char* data)
{
// 请求网络数据
int ret = -1;
if (url != "")
{
unsigned char tempRequest[32] = {0x8};
ret = send(tempRequest);
}
// 接受返回数据
if (ret > 0)
{
unsigned char tempRecv[1024] = {0x00};
recv(tempRecv, ret);
}
// 返回数据给请求发起者
if (ret > 0)
{
for(int i = 0; i < (int)sizeof(data); i++)
{
data[i] = i;
}
return ret;
}
return ret;
}
PS:纯粹方便举例,请无视示例的一些网络请求异步逻辑等处理完全没有的情况 :)
其继承自simulateSocket
类,而simulateSocket
有发送、接收的两个接口,并在network
类中封装了一个requestUrl
的接口,外部可以直接调用这个接口进行网络的请求和数据接收。
但是此时我们后台尚未能完成连接,无法得到想要的数据。那么我们可以mock
一下simulateSocket
的两个虚函数,模拟数据的请求,下面看一下如何操作:
这里使用的是gtest
框架自带的gmock
框架,其具体接口及用法可参详 官方文档,这里仅作针对该场景作简单的使用示例
class MockSimulateSocket : public simulateSocket {
public:
MockSimulateSocket(){}
virtual ~MockSimulateSocket(){}
MOCK_METHOD1(send, int(unsigned char*));
MOCK_METHOD2(recv, void(unsigned char* buff, int& len));
};
定义一个Mock
继承自需要模拟的类simulateSocket
,然后使用mock
函数的宏MOCK_THOND
,其定义说明如下:
MOCK_METHOD*(function_name, function_prototype)
*: 表示的是被mock
函数有几个参数,没有参数为0
,官方支持的参数上限是9
function_name: 表示的是被mock
函数的函数名,需要跟被mock
函数完全一致
function_prototype: 表示的是被mock
函数的返回值及参数列表,其形式如 返回值(参数1
,参数2
,……,参数9
)
其中如果没有参数则括号内可不填写空,并且参数可以只写参数类型而不用写形参(参看上面mock
的第一个参数)
然后在定义好的测试套件中使用:
1、模拟发送成功,发送了32
个字节的内容;接收失败,收到0
个字节内容
TEST_F(modelTest, showData_requestUrlFail_sendSuccess_recvFailed) {
MockSimulateSocket mock;
EXPECT_CALL(mock, send(_)).Times(1).WillOnce(Return(32));
EXPECT_CALL(mock, recv(_,_)).Times(1).WillOnce(SetArgReferee<1>(0));
pm->setNetwork((network*)&mock);
EXPECT_FALSE(pm->showData());
}
2、模拟发送成功,发送了32
个字节的内容;接收成功,收到64
个字节内容
TEST_F(modelTest, showData_requestUrlSuccess_sendSuccess_recvSuccess) {
MockSimulateSocket mock;
EXPECT_CALL(mock, send(_)).Times(1).WillOnce(Return(32));
EXPECT_CALL(mock, recv(_,_)).Times(1).WillOnce(SetArgReferee<1>(64));
pm->setNetwork((network*)&mock);
EXPECT_TRUE(pm->showData());
}
3、只模拟发送成功,不模拟接收
TEST_F(modelTest, showData_requestUrlFail_sendSuccess_nocallMockrecv) {
MockSimulateSocket mock;
EXPECT_CALL(mock, send(_)).Times(1).WillOnce(Return(32));
pm->setNetwork((network*)&mock);
EXPECT_TRUE(pm->showData());
}
用例成功通过,程序运行如预期:
下面,着重对上述用例一些新出现的gmock
用法做说明:
// 定义一个mock变量
MockSimulateSocket mock;
// 期望调用send函数1次并且send函数返回32
EXPECT_CALL(mock, send(_)).Times(1).WillOnce(Return(32));
EXPECT_CALL(mock_object, function_name()) // 期望调用宏
EXPECT_CALL
用法,其中第一个参数是mock
对象,第二个参数是期望调用的函数。send(
_
) //send
函数中,原本定义的参数是unsigned char*
类型,但这里使用了gmock
框架提供的一个通配符_
,表示这次调用不关心/不指定传入参数,由mock
自动推导。此处的测试根据函数定义所知,我们只关心send
函数的返回值就可以判断其往下执行的逻辑,并不需要关心传入什么具体内容给send
函数,所以可以使用通配符_
。Times(*) // 表示的是期望调用的次数,这里期望调用
1
次。WillOnce(Return(32)) //
WillOnce
表示一次执行的时候期望它做出什么动作响应,Return(32)
就是这次执行的期望动作:返回32
这个长度,意味着这个函数成功发送了32
个字节出去。
Return(32) //Return
是gmock
框架内自带的动作(Action
)之一,可以用来模拟函数返回值。不限于整型,如果你函数是个double
类型你可以Return(36.0)
,如果是字符串类型,你可以返回Return("xxx")
……完全可以根据实际类型进行返回,连自定义的类对象亦一样可以支持。|
// 期望调用recv函数1次并且recv函数的第一个参数(从0起索引)赋值为64。
EXPECT_CALL(mock, recv(,)).Times(1).WillOnce(SetArgReferee<1>(64));
recv
的函数mock
后的期望调用动作(Action
)是对第一个参数(从0
起索引)也就是int& len
进行赋值,表明期望recv
函数跑完之后,成功接收到64
个字节的内容。鉴于被测函数的定义,我们没有需要对接收到的内容进行额外的解析,也不需要传入特定的内容,所以
recv
调用后,我们仅需要判断收到有效长度的内容即可继续往下执行,所以这里仅需要通过int& len
返回接收到的数据长度即可。SetArgReferee(x) // 表示设定第
n
个形参的值为x
,并且该形参参数类型是引用类型。如果类型不匹配会编译出错。
pm->setNetwork((network*)&mock); // 将mock对象注入到被测对象。
因为现在被测对象中
network
对象在构造函数中new
的,所以需要使用这个函数将mock
对象注册到被测对象中。后续有一章专门讲述 [mock对象注册] 的几种方式及相应的改动点。
EXPECT_TRUE(pm->showData()); // 对被测函数进行期望断言。
EXPECT_TRUE
期望被测函数返回TRUE
,EXPECT_FALSE
期望被测函数返回FALSE
。
上述是基于gmock
框架的最简单使用,不知道各位有没有留意到,这篇文章的标题中提到的是virtual
函数的场景。没错,gmock
框架只支持虚函数的mock
,因为它的实现基础是继承,而只有虚函数才能被继承并被重写。
但是实际上,我们有很多函数都不是虚函数,那么这种情况该怎么办呢?下一篇文章即将解决的就是这种非虚函数需要mock
的场景。
更多更齐全的宏定义、Action
用法,后续有时间会陆续出小短章进行更新一些简单的示例。
对应的demo源码,请点击 mockvirtualfunc
也可扫码关注博主同名公众号"不解之榬",回复 “public” 获取