目录
3.2.1、调用次数约束(expects()/stubs())
3.3.8、mock调用次序(id、before、after)
3.3.10、清空Mock规则GlobalMockObject::verify()
1、Mock介绍
1.1、概述
mock工具的作用是指定函数的行为(模拟函数的行为)。可以对入参进行校验,对出参进行设定,还可以指定函数的返回值。例如,当我们在写代码时,涉及到模块间进行交互,双方分别提供了函数接口或消息互相发送时,使用MockCpp进行模拟函数的调用或消息的转发。再如,当我们进行FT测试校验时,涉及到模块间的交互时我们也可以使用MockCpp进行测试用例的补充与校验。
1.2、相关概念
①mock规范:每个MOCKER(function)开始,跟一系列的.stubs、.with、.will等的内容的整体,称为一个mock规范。
②核心关键字:指stubs/defaults/expects/before/with/after/will/then/id等这些直接跟在‘ . ’ 点后面的关键字。
③扩展关键字:指once()/eq()/check()/returnValue()/repeat()等这些作为核心关键字参数的关键字。
1.3、特点
① 开源。
② mockcpp支持C函数的mock和虚接口的mock。
③ VC下,mockcpp支持cdecl和stdcall调用约定的函数。(socket/bind/ftp等函数就是stdcall调用约定的)
④ mockcpp的语法清晰、简单,容易理解(参见3.1小节两个sample)。
⑤ mockcpp是强类型检查的,强类型检查也是C/C++的一个优势,比如eq(3),如果调用函数时用的参数是UL,那么就应该用eq((UL)3),对于函数返回值也是一样,声明的返回值类型应该跟mock中定义的一致。mock中支持类型检查,可能会发现更多代码BUG的。C++强类型检查,也是为了提高程序安全性,有些问题通过类型检查在编译期可以发现的,就不需要在运行时再痛苦的定位了。C/C++的强类型检查是优势,我们不用把它抛弃了。
⑥ mockcpp的错误信息提示非常友好,包含完整的mock规范定义和实际运行情况(参见下面的样例)。
Unexpected invocation: the invocation cannot be found in allowed invoking list.//没有匹配的mock规则
Invoked: add((int)0x1/1, (int)0x2/2)//实际调用add(x,y)传入的参数为1,2
Allowed:
method(add)
.stubs()
.invoked(0)//第一次调用
.with(eq((int)0xa/10), eq((int)0x14/20))//规则指定入参必须为(10,20),所以匹配失败
.will(returnValue((int)0xc8/200));
2、对C使用Mock
2.1、安装
需要按照当前我们使用的gtest模式进行编译。在mockcpp/build子目录下执行cmake,其中$xunit_home是gtest头文件所在目录(此时gtest.h路径为$xunit_home/include/gtest/gtest.h)。
cmake -DMOCKCPP_XUNIT=gtest -DMOCKCPP_XUNIT_HOME=$xunit_home ../
make
make install
安装后的头文件和库在/usr/local下面,可以不用额外指定头文件搜索路径和库文件搜索路径。后续如果觉得好用,也可以把头文件和库提取出来,指定到项目目录树中。
2.2、配置
测试工程,使用mock接口的文件需要包含:#include <mockcpp/mokc.h>,同时链接mockcpp的库:-lmockcpp
2.3、优缺点
缺点:被mock的函数,是需要事先存在的,mockcpp并不能完成自动打桩的工作。mockcpp做的,是改变或检查函数的行为,使配合我们的测试工作,虽然mockcpp的实现要复杂得多,但是其用途和原理,和《嵌入式c语言的TDD开发》里的实例是一样的。
优点:省去打桩的繁琐步骤是mock
3、Mock规范使用
3.1、sample代码
首先,请看两段mockcpp的使用规范示例代码,其中带“/”或者“|”的表示在该位置可能有多种选择;带中括号的表示是可选的。一段简单的mockcpp使用sample代码:(带有完整核心关键字)
TEST(mockcpp simple sample)
{
MOCKER(function) / MOCK_METHOD(mocker, method)
.stubs() / defaults() / expects(once())
[.before("some-mocker-id")]
[.with(eq(3))]
[.after("some-mocker-id")]
.will(returnValue(1)) / .will(repeat(1, 20))
[.then(returnValue(2))]
[.id("some-mocker-id")]
}
下面是一段比较详细的mockcpp使用sample代码:(带有完整扩展关键字)
TEST(mockcpp detail sample)
{
MOCKER(function) / MOCK_METHOD(mocker, method)
.stubs() / defaults() / expects(never() | once() | exactly(3) | atLeast(3) | atMost(3) )
[.before("some-mocker-id")]
[.with( any() | eq(3) | neq(3) | gt(3) | lt(3) | spy(var_out) | check(check_func)
| outBound(var_out) | outBoundP(var_out_addr, var_size) | mirror(var_in_addr, var_size)
| smirror(string) | contains(string) | startWith(string) | endWith(string) )]
[.after("some-mocker-id")]
.will( returnValue(1) | repeat(1, 20) | returnObjectList(r1, r2)
| invoke(func_stub) | ignoreReturnValue()
| increase(from, to) | increase(from) | throws(exception) | die(3))
[.then(returnValue(2))]
[.id("some-mocker-id")]
}
3.2、核心关键字解析
mockcpp当前的实现支持七种类型的约束/行为:(调用顺序必须按照以下序列进行)
① 调用次数约束—— expects()/stubs()
② 调用者选择器—— caller()
③ 先行调用约束—— before()
④ 调用参数约束—— with()
⑤ 后行调用约束—— after()
⑥ 函数调用行为—— will()/then()
⑦ 标识符指定—— id()
3.2.1、调用次数约束(expects()/stubs())
紧跟着MOCKER/MOCK_METHOD之后的是stubs、或者defaults、或者expects,三个必须有一个。(这是与AMOCK不同的地方,在这个层次上确定这三个关键字必须有一个,可以让mock语法更清晰)
①defaults 表示定义一个默认的mock规范,但它优先级很低;如果后面有stubs或者expects重新指定函数行为,就会按照新指定的来运行。(一般用在setup中)
②stubs 表示指定函数的行为,不校验次数。
③expects 与stubs的区别就是校验次数。格式:(.expects(once()) / .expects(never()) / .expects(exactly(123)))
一次:once()
准确的次数:exactly(n)
至少:atLeast(n)
atLeastOnce()
至多:atMost(n)
atMostOnce()
不调用:never()
注意:次数校验expects()需和GlobalMockObject::verify()一起使用才能校验生效,GlobalMockObject::verify()放在主函 数最后面)
3.2.2、调用者选择器(caller)
这个功能当前主要用于C语言的单元测试/集成测试。当一个函数会被一个流程的多个函数调用时,当其它的选择器很难以进行描述时,可以使用调用者选择器来缩小定位范围。调用者选择器通过caller(function)来指定。其中,function是一个字符串,应该等于调用函数的名字。
3.2.3、函数参数约束(with)
①入参数约束:
相等:eq(value)
不等:neq(value)
大于: gt(value)
小于: lt(vlaue)
内存匹配 :mirror(var_in_addr, var_size)用来约束内存内容的严格相等性。如果用来约束对象内容,则可以忽略size参数
对象内容监控:spy(address)读取监控对象值
字符串匹配:smirror(str) 约束字符串的相等性
startWith(str):入参以str开始
endWith(str) :入参以str结尾
占位符: any()
定制化检查:checkWith(CheckNum(num)) 内部必须是结构体、类
② 输出参数约束
通过引用设置输出参数:outBound(reference, 输入参数约束/sizeof(reference))
通过指针设置输出参数:outBoundP(pointer, 输入参数约束/sizeof(pointer))
3.2.4、调用次序(before、id、after)
①标识符指定(id):当一个调用描述用到before()/after()等调用顺序约束时,需要给被参照的调用描述一个标识符。标识符通过id(identity)指定。其中identity是一个字符串。在一个对象范围内,identity必须唯一,否则mock++则可能会引用到错误的调用描述。
②before(id):之前
③after(id):之后
注意:(before、id、after不能和stubs一起使用)
3.2.5、函数调用行为(will、then)
①will指定返回值/②then指定下一次返回值
返回一个值:returnValue(value)
重复返回一个值:repeat(value, times)
步增一个值:increase(from, to)/increase(from)
返回一系列的值:returnObjectList(o1, o2,…);依次返回o1, o2,…;超过返回列表数据个数,报错。返回个数有限。
抛出异常:throws(exception(“Exception!”))
异常退出:die表示程序退出,并且返回指定的值。用于模拟一个函数调用崩溃的情况。
忽略返回值:ignoreReturnValue(),如果函数定义的返回值类型不是void ,则一定要指定函数调用行为;即使不关心返回值,也要通过ignoreReturnValue()来指定。
转调一个stubFunction函数:invoke(stubFunction)
注意:带有返回值的函数,MOCKER后面必须有will,否则mockcpp认为无返回值,校验时会发现返回类型不匹配。
接口:char * sysMemRosBottom (void){}
mock规则:MOCKER(sysMemRosBottom).expects(once()).will(returnValue(NULL));
错误:
.../mockcpp/ChainableMockMethod.h:69: Failure
Returned type does NOT match the method declaration
Required : char*
Returned : long, which is from
method(sysMemRosBottom)
.expects(once())
.invoked(1)
.will(returnValue((long)0/0));
unknown file: Failure
C++ exception with description "failed due to mockcpp exception" thrown in the test body.
3.3、难点代码演练
3.3.1、函数调用行为指定返回值
will指定函数的返回值:.will(returnValue(expect_var));如果要多次重复的相同值,则用.will(repeat(expect_var, repeat_cnt)),若果超过返回次数,后面没有接then,则报错;需要指定下一次返回值则用.then(returnValue(expect_var));具体见如下代码:
int i=0,repeat_cnt = 20,expect_var = 100;
//入参1,2:前repeat_cnt次,返回值为expect_var;第repeat_cnt+1次,返回值expect_var+1
MOCKER(add).defaults().with(eq(1), eq(2)).will(repeat(expect_var, repeat_cnt)).then(returnValue(expect_var+1));
//入参3,4:第一次返回值为expect_var;第2,3,4....次,返回值expect_var+1
MOCKER(add).defaults().with(eq(3), eq(4)).will(returnValue(expect_var)).then(returnValue(expect_var+1));
for(i = 0; i < repeat_cnt; i++)
{
EXPECT_EQ(expect_var, add(1,2));
}
EXPECT_EQ(expect_var + 1, add(1,2));
EXPECT_EQ(expect_var, add(3,4));
EXPECT_EQ(expect_var+1, add(3,4));//之后的返回值均为expect_var+1
EXPECT_EQ(expect_var+1, add(3,4));//第3次
GlobalMockObject::verify();
3.3.2、入参数约束之内存监控(参数类型也参与检查)
当函数的入参为一个指针时,此时eq(var)则无法校验指针指向的地址的内容,改用mirror(var_in_addr, var_size),用于监控内存地址中的内容,类似C标准库中的int memcmp(const void *s1, const void *s2, size_t n),参数类型必须一致(显示强转),否则匹配失败,;具体代码如下 :
int testMethodInParaPointer(char* str)
{
return 0;
}
int callTestMethodInParaPointer()
{
char temp[10]={"hello"};
return testMethodInParaPointer(temp);
}
TEST(RosMemBuf_adjust_capacity_T, GIVEN_ANY_WHEN_CALL_testMethodInParaPointer_THEN_OK)
{
int expect_var = 100;
/*eq(var),全局变量和局部变量不同无法匹配,显示:the invocation cannot be found in allowed invoking list*/
//MOCKER(testMethodInParaPointer).defaults().with(eq((char*)"hello")).will(returnValue(expect_var));
/*字符串"hello"需要加(char *),否则解析成(char const *)依然无法匹配*/
MOCKER(testMethodInParaPointer).defaults().with(mirror((char *)"hello",strlen("hello"))).will(returnValue(expect_var));
EXPECT_EQ(expect_var,callTestMethodInParaPointer());
GlobalMockObject::verify();
}
3.3.3、同一个函数指定多个mock规范
当对同一个函数指定多个mock规范,那么这些mock规范一般是应该在输入方面有差异的,否则没必要指定多个。而且,在输入方面无差异的两个mock规范,让mockcpp内部无法判断是否满足规范约束。 比如,测试函数 void testMtehodParaInOut(int input,int *p_output),前面一个是入参,后面一个是出参,希望input为10时,输出*p_output为100,input为11时,输出*p_output为111,那么应该这样写:
int in_var = 10,expect_var = 100,real_var1 = 0;
MOCKER(testMtehodParaInOut).expects(once()).with(eq(in_var),outBoundP(&expect_var,sizeof(expect_var)));
testMtehodParaInOut(in_var,&real_var1);
EXPECT_EQ(expect_var, real_var1);
expect_var = 111;
MOCKER(testMtehodParaInOut).expects(once()).with(eq(in_var+1),outBoundP(&expect_var,sizeof(expect_var)));
testMtehodParaInOut(in_var+1,&real_var1);
EXPECT_EQ(expect_var, real_var1);
GlobalMockObject::verify();
假设你把eq(in_var)、eq(in_var+1)写为any(),那么mockcpp会判断你的两个调用testMtehodParaInOut(in_var,&real_var1);和testMtehodParaInOut(in_var,&real_var1);都符合第一个mock规范,从而报告与你定义的次数1不匹配。或者仅有一条mock规范(此时outBoundP相同),但是实际调用两次保同样的错,信息如下:
.../ChainableMockMethod.h:69: Failure
Invocation is expected only once(), but you are trying to invoke more than that
method(testMtehodParaInOut)
.expects(once())
.invoked(1)//实际调用次数
.with(any(), outBoundP((int*)0x00ada680));
unknown file: Failure
C++ exception with description "failed due to mockcpp exception" thrown in the test body.
3.3.4、入参约束之对象内容监控(spy)
spy的作用是监视,下面例子,就是监视test在调用add时,传入的值是多少。有些时候,测试用例需要这样的值,这种方式是很有效的。
int var_out1, var_out2,expect_var = 100;
MOCKER(add).defaults().with(spy(var_out1),spy(var_out2)).will(returnValue(expect_var));
EXPECT_EQ(expect_var, add(3,4));
EXPECT_EQ(var_out1, 3);//监控到第一个参数为3
EXPECT_EQ(var_out2, 4);//监控到第二个参数为4
3.3.5、入参约束之定制化检查(check)
约束关键字中,有很多都是对入参进行检查的,比如eq、neq、lt、gt等等,但它们都只实现了非常简单的检查。 如果用户想做一个定制化的检查,那么就可以用check,但是编译报错 error: 'check' was not declared in this scope,原因:mockcpp在2.4版本后没有了check关键字,代替的是checkWith注意大小写,以为还有一个CheckWith。 举一个例子:假设用户想检查入参p指向的结构的字段b是否大于10,那么可以如下这样写:
typedef struct data_in{
int a;
int b;
}def_data_in;
//定义在C++中,不要定义在C中,编译无法识别bool(小写),
bool checkTestMockcppCheckEx1(def_data_in *pdata){
if(pdata->a >10)
return true;
else
return false;
}
//实现一个检查函数,入参类型跟要检查的入参相同,返回值为bool,返回true表示检查通过。
bool checkTestMockcppCheckEx2(def_data_in *pdata){
if(pdata->b == 10)
return true;
else
return false;
}
int testMockcppCheck(def_data_in *pdata1, def_data_in *pdata2 ){
return 0;
}
TEST(xxxxx, DoubleParaTest)
{
def_data_in g_data1={11,9},g_data2={11,10};
int expect_var = 100;
MOCKER(testMockcppCheck).stubs()
.with(checkWith(checkTestMockcppCheckEx1),checkWith(checkTestMockcppCheckEx2))//将校验参数填写到原来的参数限定位置
.will(returnValue(expect_var));
EXPECT_EQ(expect_var, testMockcppCheck(&g_data1,&g_data2));
}
int func(int in, def_data_in *p)
{
return 0;//mockcpp对类型进行检查,对于有返回值类型的接口,没有return操作,打桩后会出现Segmentation fault
}
TEST(xxx, SingleParaTest)
{
def_data_in var={1,10};
int expect_var = 100;
MOCKER(func)
.stubs()
.with(any(),checkWith(checkTestMockcppCheckEx2))// error: 'check' was not declared in this scope,mockcpp在2.4版本后没有了check关键字,注意大小写,以为还有一个CheckWith
.will(returnValue(expect_var));
EXPECT_EQ(expect_var, func(1, &var));
}
3.3.6、指针设置输出参数outBoundP
实际打桩过程中,经常需要指定接口出参的数值,当出参的类型为指针时则需要设置指针指向地址的内容,此时需要通过outBoundP来指定,具体代码如下:
void testMtehodParaInOut(int input,int *p_output)
{
*p_output = input +1;
}
...
int in_var = 10,expect_var = 12,real_var1 = 0;
MOCKER(testMtehodParaInOut).stubs().with(eq(in_var),outBoundP(&expect_var,sizeof(expect_var))); //指定出参的指针内容为12
testMtehodParaInOut(in_var,&real_var1);//调用后,real_var1的指针内容为12
EXPECT_EQ(expect_var, real_var1);
3.3.7、mock规则匹配失败
如果要对某个函数(比如add)指定多个调用不同的行为(多个mock规范),比如调用入参为1,2时,返回30;调用入参为3、4时,返回300。那么可以这样写:
MOCKER(add).stubs().with(eq(1), eq(2)).will(returnValue(30));
MOCKER(add).stubs().with(eq(3), eq(4)).will(returnValue(300));
EXPECT_EQ(300, add(1,4));//没有匹配的mock规则,运行报错
GlobalMockObject::verify();
只有参数满足条件桩函数才会生效,如果调用接口(add)没有匹配到mock规则,则报错(意外调用:在允许的调用列表中找不到调用),具体如下:
....../mockcpp/ChainableMockMethod.h:69: Failure
=====================================
Unexpected invocation: the invocation cannot be found in allowed invoking list.
Invoked: add((int)0x1/1, (int)0x4/4)
Allowed:
method(add)
.stubs()
.invoked(0)
.with(eq((int)0x1/1), eq((int)0x2/2))
.will(returnValue((int)0x1e/30));
method(add)
.stubs()
.invoked(0)
.with(eq((int)0x3/3), eq((int)0x4/4))
.will(returnValue((int)0x12c/300));
=====================================
unknown file: Failure
C++ exception with description "failed due to mockcpp exception" thrown in the test body.
结论:只要打桩了,那么就不会运行原函数,都是与mock规范匹配,运行桩,否则运行报错
3.3.8、mock调用次序(id、before、after)
如果上面的3.3.7小结的例子,希望对调用顺序做严格要求,必须第一次调用参数是1、2,第二次是3、4,那么可以这样写:
MOCKER(add)
.stubs()
.with(eq(1), eq(2))
.will(returnValue(30))
.id("first");
MOCKER(add)
.stubs()
.with(eq(3), eq(4))
.after("first")
.will(returnValue(700));
自定义规则:假设有时候需要对同一个函数指定多个mock规范,并且它们是有规律的,那么可以借助循环来简化代码。 假设要mock的函数是void function(int in),希望它依次以0、1、2...10为入参被调用,每次都调用一次,那么可以像这样写:
MOCKER(function)
.expects(once())
.with(eq(0))
.id("0");
for (int i = 1; i <= 10; i++)
{
MOCKER(function)
.expects(once())
.with(eq(i))
.after(string(i - 1))
.id(string(i));
}
//没有实验,编译失败,版本(2.6)无法编译mock规则的id、after、before等着字段,显示:error: 'struct mockcpp::StubBuilder<mockcpp::MoreStubBuilder<mockcpp::DummyBuilder> >' has no member named 'after'。查阅对应的ChainingMockHelper.h,发现确实没有实现。需要更新版本
3.3.9、mock实际调用次数
在实际打桩过程我们可以通过expects(exactly(x))指定其精确运行x次,超过则报错,代码如下
MOCKER(add).expects(exactly(2)).with(eq(100), eq(200)).will(returnValue(30));
EXPECT_EQ(30, add(100,200));
EXPECT_EQ(30, add(100,200));
EXPECT_EQ(30, add(100,200));
GlobalMockObject::verify();
提示错误信息:
.../ChainableMockObjectBase.cpp:85: Failure
Expected invoking exactly 2 times, but it's actually invoked 3 times.
method(add)
.expects(exactly(2))
.invoked(3)//实际第3次调用
.with(eq((int)0x64/100), eq((int)0xc8/200))
.will(returnValue((int)0x1e/30));
unknown file: Failure
C++ exception with description "failed due to mockcpp exception" thrown in the test body.
3.3.10、清空Mock规则GlobalMockObject::verify()
使用mockcpp时,校验是否按照mock规范进行调用的,应该用:GlobalMockObject::verify();verify之后,会自动执行reset。通常在进行Unit测试时,需要避免用例之间的联系,不能上一个用例的桩函数在本例生效,需要在用例的结尾处执行该语句。