为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,作者编写了一个简单实用的mock工具,在此开源分享(代码详见附录)。
背景
在单元测试中,往往需要减少被测函数的外部依赖,如网络访问、数据库访问等。我们希望有一个mock工具能让我们轻松地屏蔽掉外部依赖。
C++的开源mock工具比较少,而且大多是基于多态实现的(如gmock),只支持mock虚函数,需要对原有代码结构进行调整,或编写mock类继承自原有类才能使用,工作量太大,我们的目标是单测代码与原有项目工程隔离,不需要为了单测对线上代码逻辑进行太大修改。为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,编写了一个简单实用的mock工具,在此开源分享,希望能在单测工程的搭建上有所帮助,大家可以根据自己的项目情况,在此基础上扩展自己需要的功能。
Frida是一种动态插桩工具,可以插入一些代码到Win、Mac、Linux、Android或者iOS原生app的内存空间,动态地监视和修改其行为,在安卓逆向工程上应用非常多。
我们的mock工具也正是利用了Frida能动态修改函数执行的功能,在此我们不对其具体原理做过多阐述,有兴趣的同学可以移步附录的参考链接。
实现的功能
mock工具实现了最实用的函数替换功能,支持所有类型的函数替换,如:成员函数、虚函数、静态函数、系统库的全局函数、链接库的函数,暂时不支持对匿名函数进行替换(需要拿到函数指针才行)。
▐ MOCK宏
MOCK(target, alter)
第一个参数为被mock的函数指针,第二个参数为想要替换的lambda或者函数指针。
▐ MOCK_RETURN宏
MOCK_RETURN(target, alter)
对MOCK宏的封装,替换掉被mock函数的返回值,只支持值类型。
▐ ExpectTimes
被mock函数期望的调用次数。
MOCK_RETURN(&MyTest::testMember, 0)->ExpectTimes(1); //测试运行次数
测试用例运行完成时,如果被mock的函数运行次数和期望的次数不一样,则测试用例运行失败。
▐ 自动回滚mock
TEST(mock, mock脱离作用域自动失效) {
int input = 10; //入参
{
MOCK_RETURN(&globalFunction, 100);
ASSERT_EQ(globalFunction(input), 100); //修改了返回值
}
ASSERT_EQ(globalFunction(input), input); //mock析构后失效
}
利用此特性可以使单元测试每个测试用例相对独立,所以在一个测试用例中mock掉的函数,不影响其他用例,如果需要全局性的mock,需要将mock宏写在全局变量区域。
▐ 跨平台
在MacOS、Linux、Windows、Android、iOS等平台都能使用。
使用方式示例
下面的示例都基于这个类来使用mock
/**
* 被mock的类
*/
class MyTest {
public:
/**
* 静态函数
* @return
*/
static int testStatic() {
return 0;
}
/**
* 普通成员函数
* @param input
* @return
*/
int testMember(int input) {
return input;
}
/**
* const 成员函数
* @return
*/
int constMemberFun() const {
return 0;
}
/**
* 测试函数重载,输入int
* @param intInput
* @return
*/
static int overloadFunction(int intInput) {
return 0;
}
static int overloadFunction(double doubleInput) {
return 0;
}
};
▐ mock静态函数
// 将testStatic的返回值改为10
MOCK(&MyTest::testStatic, []() {
return 10; // 返回值从0修改为10
});
// 简单写法,效果同上,使用MOCK_RETURN 可以替换函数返回值
MOCK_RETURN(&MyTest::testStatic, 10);
▐ mock成员函数
// 将testMember的返回值改为 input + 1
MOCK(&MyTest::testMember, [](auto && self, auto && input) {
// 成员函数被mock后,第一个参数为this指针,这里用self替换
return input + 1; //mock后返回input + 1(之前是返回input)
});
int input = 10; //入参
ASSERT_EQ(MyTest().testMember(input), input + 1);
▐ mock函数重载
// 使用static_cast指定对应的重载获取函数指针
MOCK(static_cast<int(*)(int)>(&MyTest::overloadFunction), [](auto && ...args) {
return 100;
});
ASSERT_EQ(MyTest().overloadFunction(10), 100);
ASSERT_EQ(MyTest().overloadFunction(10.0), 0);
▐ 小结
通过MOCK宏来替换整个函数
通过MOCK_RETURN宏来替换函数返回值
可以使用 auto&& 简化你的函数入参类型
什么时候适合使用mock
考虑在以下场景使用mock工具,可以减少你的单元测试成本:
代码中依赖的某个功能在你本次测试并不关心,如:db数据读取,发请求
测试用例依赖一些复杂的数据源,如:db数据读取,流水线上游数据,网络请求
一些非幂等性的函数调用或者结果返回不稳定的函数调用,如:随机数获取,时间获取,db写入
对象的某些状态难以创建或者重现,如:网络错误或者文件读写错误
验证一些中间过程值,如:你的函数没有返回值,或者中间过程值不方便验证,可以mock中间某个函数调用来验证中间过程结果是否正确
注意事项
成员函数第一个函数入参为this指针
在新的函数中抛出的异常无法被原有的上层正常catch
断点调试时行号可能对不上,手动解除宏的封装可以解决,原因未知
建议单测工程不要开优化,否则会导致一些函数mock失败
Frida会被内存泄漏检测工具报内存泄漏
对动态库使用mock工具时,库编译时需要加上 -fPIC 参数
附录
▐ 参考文档
「frida git仓库」https://github.com/frida/frida
「Frida工作原理学习」https://www.wangan.com/p/7fy7f8bd4ab57950
「frida-gum代码阅读笔记」https://o0xmuhe.github.io/2019/11/15/frida-gum%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/
「FRIDA-GUM源码解读」https://jmpews.github.io/2017/06/27/pwn/frida-gum%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/
▐ 代码
请自行下载对应平台的frida-gum头文件&链接库:https://github.com/frida/frida/releases
代码中使用了gtest,可以视情况决定需要是否保留#pragma once
#include <frida/frida-gum.h>
#include <functional>
#include <memory>
#include <string>
#include <type_traits>
#include "gtest/gtest.h" // 使用了 EXPECT_EQ, 可以视情况保留
#define _COMBINE_(X, Y) X##Y // helper macro
#define COMBINE(X, Y) _COMBINE_(X,Y)
#define MOCK(target, alter) auto && COMBINE(mock, __LINE__) = mock::Mock(target, alter)
#define MOCK_RETURN(target, alter) auto && COMBINE(mock, __LINE__) = mock::MockReturn(target, alter)
namespace {
/**
* 将任意对象转换成void *
* 用于函数指针的转换
* @tparam T
* @param t
* @return 转换成void * 的函数指针
*/
template<typename T>
void * toVoidPtr(T src) {
union {
void * ptr;
T src;
} Func{};
Func.src = src;
return Func.ptr;
}
}
namespace mock {
/**
* 一个对象管理一个mock,析构时自动回滚
* @tparam RET
* @tparam ARGS
*/
template<typename RET, typename ...ARGS>
class MockHandler : public std::enable_shared_from_this<MockHandler<RET, ARGS...>> {
public:
/**
* 这个函数作为回调函数传给frida
* @param args
* @return
*/
static RET _invoke(ARGS... args) {
auto * context = gum_interceptor_get_current_invocation();
auto * handler = reinterpret_cast<MockHandler<RET, ARGS...> *>(
gum_invocation_context_get_replacement_data(context));
++handler->mRunTimes;
return handler->mAlterFun(std::forward<ARGS>(args)...);
}
MockHandler(void * target, const std::function<RET(ARGS...)> & fun) : mAlterFun(fun), mTarget(target) {
gum_init_embedded();
mInterceptor = gum_interceptor_obtain();
gum_interceptor_begin_transaction(mInterceptor);
gum_interceptor_replace(mInterceptor, mTarget, toVoidPtr(_invoke), toVoidPtr(this));
gum_interceptor_end_transaction(mInterceptor);
}
/**
* 析构时会回滚已经替换掉的函数,实现测试用例隔离
*/
~MockHandler() {
if (mInterceptor == nullptr) {
return;
}
gum_interceptor_begin_transaction(mInterceptor);
gum_interceptor_revert(mInterceptor, mTarget);
gum_interceptor_end_transaction(mInterceptor);
g_object_unref(mInterceptor);
if (mExpectRunTimes >= 0) {
EXPECT_EQ(mExpectRunTimes, mRunTimes) << "运行次数和期望不相等";
}
}
MockHandler(const MockHandler &) = delete;
MockHandler & operator=(const MockHandler &) = delete;
MockHandler(MockHandler &&) = delete;
MockHandler & operator=(MockHandler &&) = delete;
/**
* 设置期望的运行次数
* @param times
* @return this指针
*/
auto ExpectTimes(int times) {
mExpectRunTimes = times;
return std::enable_shared_from_this<MockHandler<RET, ARGS...>>::shared_from_this();
}
private:
GumInterceptor * mInterceptor{};
//目标函数地址
void * mTarget{};
//用于替换的函数
std::function<RET(ARGS...)> mAlterFun;
//期望的运行次数
int mExpectRunTimes{-1};
//被mock函数已经运行的次数
std::atomic_int mRunTimes{};
};
/**
* 使用alter替换掉target函数指针,alter可以是lambda,也可以是函数指针
* @tparam RET
* @tparam CLS
* @tparam ARG
* @tparam T
* @param target
* @param alter
* @return MockHandler
*/
template<typename RET, class CLS, typename ...ARG, typename T>
auto Mock(RET(CLS::* target)(ARG...), T alter) {
return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), alter);;
}
template<typename RET, class CLS, typename ...ARG, typename T>
auto Mock(RET(CLS::* target)(ARG...) const, T alter) {
return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), alter);
}
template<typename RET, typename ...ARG, typename T>
auto Mock(RET(* target)(ARG...), T alter) {
return std::make_shared<MockHandler<RET, ARG...>>(toVoidPtr(target), alter);;
}
/**
* mock返回值,mock的简便用法
* @tparam RET
* @tparam CLS
* @tparam ARG
* @param target
* @param ret
* @return MockHandler
*/
template<typename RET, class CLS, typename ...ARG>
auto MockReturn(RET(CLS::* target)(ARG...), RET ret) {
return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), [=](auto && ...) {
return ret;
});
}
template<typename RET, class CLS, typename ...ARG>
auto MockReturn(RET(CLS::* target)(ARG...) const, RET ret) {
return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), [=](auto && ...) {
return ret;
});
}
template<typename RET, typename ...ARG>
auto MockReturn(RET(target)(ARG...), RET ret) {
return std::make_unique<MockHandler<RET, ARG...>>(toVoidPtr(target), [=](auto && ...) {
return ret;
});
}
}
# CMakeLists.txt
add_link_options(-lresolv -lpthread -ldl)
target_link_libraries(${YOUR_PROJECT} ${CMAKE_CURRENT_SOURCE_DIR}/libfrida-gum.a)
团队介绍
我们是大淘宝技术全域触达&用户互动客户端团队,负责包含Push、POP弹层和消息沟通三大触达场景。全域触达&用户互动客户端团队追求极致的性能、流畅的交互体验和稳定的触达效率,用智能化的调控策略为用户带来更好的用户体验。
¤ 拓展阅读 ¤
3DXR技术 | 终端技术 | 音视频技术
服务端技术 | 技术质量 | 数据算法