先说一下我的问题,一直在做的程序没有单元测试的代码,现在希望增加单元测试功能。考察了一些平台和技术,记录一下。
我的程序的结构如下
程序包含a,b两个dll,其中b依赖a。现在希望对b进行单元测试。
a.dll的示例代码如下
struct s
{
int sa;
int sb;
int sc;
};
class a{
public:
a(int i)
{
};
void afun(s* ss)
{
ss->sc=ss->sa+ss->sb;
};
static a aa;
};
a a::aa(1);
b.dll的示例代码如下:
class b{
public:
void bfun()
{
s sss;
sss.sa=1;
sss.sb=2;
a::aa.afun(&sss);
ret=sss.sc;
}
int ret;
};
由于现有的结构,测试代码不会修改原来的dll代码。而要调试b.dll,希望其中的sss返回不同的值来测试。这就需要对afun进行mock。预期的测试代码是这样的:
mock(a.aa.afun);//mock afun函数,直接修改ss->sc的值为10
b bb;
bb.bfun();
EXPECT_EQ(10,bb.ret);
试了以下几种方案:
1.gmock
这个比较有名,跟gtest配套使用,但是却不能解决我的问题。gmock通过继承要mock的类,并且把新的类传递到测试类中。也就是要求a类中的afun必须是虚函数,而且b的函数中需要以a类为参数,这样就可以将mock的类传入。显然两条都不符合。
2.mockcpp
这个可以mock静态的c函数,还可以mock类的静态成员函数,但是还不知道怎么mock非静态函数。并且另一个问题,只能指定返回值,没法修改参数的值。
3.经过了漫长的搜索之后,终于发现了一个有用的技术。叫inline hook,通过直接在运行时修改函数的第一条指令跳转到新的函数执行。还找到了一个实用的代码,参考c++ 单元测试打桩技巧总结(stub、mock)
经过测试,实用这个库里的代码能够实现我要的效果。
同时,记录一下调试过程中出现的错误。
1.Run-Time Check Failure #0 错误
在使用中,出现了这个错误,然后程序就崩溃了。
经过搜索,并查看了汇编代码。发现了这个错误检查的用处。
这个错误是由于_RTC_CheckEsp_检查没通过。那么这个检查又是干嘛的呢。查看https://blog.csdn.net/wupangzi/article/details/7032356
通过汇编代码可以看到原因。
在调用函数之前,会保持esp的值,调用函数返回之后,再查看一下esp的值,正常情况下应该保持不变,否则就会报这个错误。
那么为什么我的代码会出现不一致呢。仔细检查才发现,多了4个字节。原来我要替代的函数定义有两个参数,但是第二个参数时有默认值的,所以调用的时候只传入了一个参数,而我写的桩函数把这个给忘了,只写了一个参数。这就导致了调用时压栈两个,调用完了出栈一个,正好差了4。
这个错误也大部分是由于函数参数不一致导致的。
参考:
1.c++ 单元测试打桩技巧总结(stub、mock) 引用地址https://blog.csdn.net/coolxv_6533/article/details/79550197
Stub API 源码地址: https://github.com/coolxv/cpp-stub
说明:
- 只适用linux,和windows的x86、x64架构
- access private function相关方法基于C++11(参考:https://github.com/martong/access_private)
- replace function相关方法基于C++03
- windows和linux的用法会稍微不同,原因是获取不同类型函数地址的方法不同,且调用约定有时不一样
不可以打桩的情况:
- 不可以对exit函数打桩,编译器做了特殊优化
- 不可以对纯虚函数打桩,纯虚函数没有地址
- static声明的普通内部函数不能打桩,内部函数地址不可见(解析ELF或许可以获得函数地址)
。。。。。。
2.关于_RTC_CheckEsp_ 引用地址https://blog.csdn.net/no_lock/article/details/46696095
今天工作时,反汇编中注意到_RTC_CheckEsp_这个函数,查了查资料,发觉这是个运行时检查函数,应该是用来检查缓冲区溢出的。
具体代码如下:
0135154B int 3
0135154C int 3
0135154D int 3
0135154E int 3
0135154F int 3
_RTC_CheckEsp:
01351550 jne esperror (1351553h)
01351552 ret
esperror:
01351553 push ebp
01351554 mov ebp,esp
调用出现的位置有:
printf("input a number: ");
013513BE mov esi,esp
013513C0 push offset string "input a number: " (135575Ch)
013513C5 call dword ptr [__imp__printf (13582B8h)]
013513CB add esp,4
013513CE cmp esi,esp
013513D0 call @ILT+305(__RTC_CheckEsp) (1351136h)
scanf("%d",&n);
013513D5 mov esi,esp
013513D7 lea eax,[n]
013513DA push eax
013513DB push offset string "%d\n" (1355758h)
013513E0 call dword ptr [__imp__scanf (13582B4h)]
013513E6 add esp,8
013513E9 cmp esi,esp
013513EB call @ILT+305(__RTC_CheckEsp) (1351136h)
printf和scanf函数都会先保存esp至esi中,printf/scanf函数调用结束后,调用_RTC_CheckEsp检查值有没有发生改变。
函数结束ret过程中也有类似操作,不过是esp和ebp进行比较。
3浅析VS编译开关: /RTCc、/RTCu、/RTCs 引用地址https://blog.csdn.net/wupangzi/article/details/7032356