在面向对象的开发过程中,由于需要将各种属性或者事物按一定的规律抽象为独立的一个对象,然后按需进行调用,如此一来,对象之间的依赖便无可避免,设计不好更会产生双向依赖、交叉依赖等困境,那么我们在面对这种对象间依赖的情况下,该如何进行单元测试呢?
设想一个场景:如果我们正在开发的模块有个处理,需要先向后台请求一些数据,然后再根据后台响应的数据,进行一定的处理,
但是,我们后台还没开发完成,预计还需要几个月才能搭起一个可连接的环境,我们总不能等到几个月后才开展工作吧?
面对上述类似的这种情况,我们可以在单元测试中引入一个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());
}
用例成功通过,程序运行如预期:
下面,着重对上述用例一些新出现的点做说明:
MockSimulateSocket mock; // 定义一个mock变量。 EXPECT_CALL(mock, send(_)).Times(1).WillOnce(Return(32)); // 期望调用send函数1次并且send函数返回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")……完全可以根据实际类型进行返回,连自定义的类对象亦一样可以支持。 | | EXPECT_CALL(mock, recv(,)).Times(1).WillOnce(SetArgReferee<1>(64)); // 期望调用recv函数1次并且recv函数的第一个参数(从0起索引)赋值为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