独望枫

人在尘世间,有缘自相见,变化千千万,未开窍,已迷恋
posts - 45, comments - 0, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

在面向对象的开发过程中,由于需要将各种属性或者事物按一定的规律抽象为独立的一个对象,然后按需进行调用,如此一来,对象之间的依赖便无可避免,设计不好更会产生双向依赖、交叉依赖等困境,那么我们在面对这种对象间依赖的情况下,该如何进行单元测试呢?

设想一个场景:如果我们正在开发的模块有个处理,需要先向后台请求一些数据,然后再根据后台响应的数据,进行一定的处理,

但是,我们后台还没开发完成,预计还需要几个月才能搭起一个可连接的环境,我们总不能等到几个月后才开展工作吧?

面对上述类似的这种情况,我们可以在单元测试中引入一个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) // Returngmock框架内自带的动作(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期望被测函数返回TRUEEXPECT_FALSE期望被测函数返回FALSE

上述是基于gmock框架的最简单使用,不知道各位有没有留意到,这篇文章的标题中提到的是virtual函数的场景。没错,gmock框架只支持虚函数的mock,因为它的实现基础是继承,而只有虚函数才能被继承并被重写。

但是实际上,我们有很多函数都不是虚函数,那么这种情况该怎么办呢?下一篇文章即将解决的就是这种非虚函数需要mock的场景。

 

更多更齐全的宏定义、Action用法,后续有时间会陆续出小短章进行更新一些简单的示例。

 

对应的demo源码,请点击mockvirtualfunc


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理