实现无入侵式C++代码mock工具

news2024/12/23 9:25:40

2023a2a7becd98ddaefe9c0180ffdf11.gif

为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,作者编写了一个简单实用的mock工具,在此开源分享(代码详见附录)。

a0b152b1f8c089c405cba80b717b1399.png

背景

在单元测试中,往往需要减少被测函数的外部依赖,如网络访问、数据库访问等。我们希望有一个mock工具能让我们轻松地屏蔽掉外部依赖。

C++的开源mock工具比较少,而且大多是基于多态实现的(如gmock),只支持mock虚函数,需要对原有代码结构进行调整,或编写mock类继承自原有类才能使用,工作量太大,我们的目标是单测代码与原有项目工程隔离,不需要为了单测对线上代码逻辑进行太大修改。为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,编写了一个简单实用的mock工具,在此开源分享,希望能在单测工程的搭建上有所帮助,大家可以根据自己的项目情况,在此基础上扩展自己需要的功能。

Frida是一种动态插桩工具,可以插入一些代码到Win、Mac、Linux、Android或者iOS原生app的内存空间,动态地监视和修改其行为,在安卓逆向工程上应用非常多。

我们的mock工具也正是利用了Frida能动态修改函数执行的功能,在此我们不对其具体原理做过多阐述,有兴趣的同学可以移步附录的参考链接。

cf67342f7e3f984968121d0386c7de1c.png

实现的功能

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等平台都能使用。

c5f9161456d477cc416b1463c721b179.png

使用方式示例

下面的示例都基于这个类来使用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);

  小结

  1. 通过MOCK宏来替换整个函数

  2. 通过MOCK_RETURN宏来替换函数返回值

  3. 可以使用 auto&& 简化你的函数入参类型

d8651664dcd7a10cfd48db4fe831078a.png

什么时候适合使用mock

考虑在以下场景使用mock工具,可以减少你的单元测试成本:

  1. 代码中依赖的某个功能在你本次测试并不关心,如:db数据读取,发请求

  2. 测试用例依赖一些复杂的数据源,如:db数据读取,流水线上游数据,网络请求

  3. 一些非幂等性的函数调用或者结果返回不稳定的函数调用,如:随机数获取,时间获取,db写入

  4. 对象的某些状态难以创建或者重现,如:网络错误或者文件读写错误

  5. 验证一些中间过程值,如:你的函数没有返回值,或者中间过程值不方便验证,可以mock中间某个函数调用来验证中间过程结果是否正确

178d894cf256607fdb2a930f962c4870.png

注意事项

  1. 成员函数第一个函数入参为this指针

  2. 在新的函数中抛出的异常无法被原有的上层正常catch

  3. 断点调试时行号可能对不上,手动解除宏的封装可以解决,原因未知

  4. 建议单测工程不要开优化,否则会导致一些函数mock失败

  5. Frida会被内存泄漏检测工具报内存泄漏

  6. 对动态库使用mock工具时,库编译时需要加上 -fPIC 参数

5fe29ca29ab8761d66f1f23a234cbdc9.png

附录

  参考文档

  1. 「frida git仓库」https://github.com/frida/frida

  2. 「Frida工作原理学习」https://www.wangan.com/p/7fy7f8bd4ab57950

  3. 「frida-gum代码阅读笔记」https://o0xmuhe.github.io/2019/11/15/frida-gum%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/

  4. 「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)

399f24a41a8e0ad97d8a813a4ecd32df.png

团队介绍

我们是大淘宝技术全域触达&用户互动客户端团队,负责包含Push、POP弹层和消息沟通三大触达场景。全域触达&用户互动客户端团队追求极致的性能、流畅的交互体验和稳定的触达效率,用智能化的调控策略为用户带来更好的用户体验。



¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/77595.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何实现高性能网络编程-ChatGPT怎么看

hi ,大家好&#xff0c;我是大师兄。听说最近chatgpt特别火&#xff0c;那我们邀请一下chatgpt如何实现&#xff1a;我们先来小试牛刀&#xff1a;刚开始用先用英文交流一下&#xff0c;然后试一下中文&#xff1a;元芳你怎么看&#xff1f;下期直播主题--网络编程 (如何实现高…

用 AWTK 和 AWPLC 快速开发嵌入式应用程序 (7)- 用状态机实现红绿灯

AWPLC 目前还处于开发阶段的早期&#xff0c;写这个系列文章的目的&#xff0c;除了用来验证目前所做的工作外&#xff0c;还希望得到大家的指点和反馈。如果您有任何疑问和建议&#xff0c;请在评论区留言。 1. 背景 AWTK 全称 Toolkit AnyWhere&#xff0c;是 ZLG 开发的开源…

kaggle实战:基于超市消费数据的用户个性化分析案例

大家好&#xff0c;今天给大家分享一篇 kaggle 数据集的新文章&#xff1a;基于一份超市消费数据集的用户个性化分析以及用户分群的实现。 更多详细内容参考原数据集地址&#xff1a; https://www.kaggle.com/code/sonalisingh1411/customer-personality-analysis-segmentati…

实验八 网络优化与正则化(3)不同优化算法比较

目录7.3 不同优化算法的比较分析7.3.1 优化算法的实验设定7.3.1.1 2D可视化实验7.3.1.2 简单拟合实验7.3.1.3 与Torch API对比&#xff0c;验证正确性7.3.2 学习率调整7.3.2.1 AdaGrad算法7.3.2.2 RMSprop算法7.3.3 梯度估计修正7.3.3.1 动量法7.3.3.2 Adam算法7.3.4 不同优化器…

java基于Springboot的简历系统-计算机毕业设计

项目介绍 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;简历系统当然也不能排除在外。简历系统是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;…

SAP ABAP CDS view Association 引入的缘由

ABAP CDS view 支持三种 join 方式&#xff1a; Inner JoinLeft Outer joinRight outer join 我们使用 ABAP Development Tool 的 CDS view 向导创建一个 CDS view&#xff1a; 向导里包含的 $ 和大括号就是占位符&#xff0c;需要开发人员自己指定&#xff1a; 我们把占位符…

奇舞周刊475期:2022年 CSS 生态圈技术趋势!

记得点击文章末尾的“ 阅读原文 ”查看哟~下面先一起看下本期周刊 摘要 吧~奇舞推荐■ ■ ■2022年 CSS 生态圈技术趋势&#xff01;一年一度的 State of CSS 调查结果正式公布&#xff01;通过本文看看2022年 CSS 生态圈的技术趋势&#xff01;React Streaming SSR 原理解析Re…

BI技巧丨RANKX浮点运算

RANKX这个函数&#xff0c;白茶之前已经写过很多期了&#xff0c;本期是对RANKX函数一个细节问题的补充。 我们常见的数据类型有很多&#xff0c;用来聚合的主要有三种数据类型&#xff1a;文本、整数、小数。 在大部分场合&#xff0c;小数是实际FACT数据中最为常见的数据类…

[1180]clickhouse查看数据库和表的容量大小

文章目录1.查看数据库容量、行数、压缩率2.查看数据表容量、行数、压缩率3.查看数据表分区信息4.查看数据表字段的信息5. 查看表的各个指标6.跟踪分区7.检查数据大小在mysql中information_schema这个数据库中保存了mysql服务器所有数据库的信息&#xff0c; 而在clickhouse&…

[附源码]Python计算机毕业设计SSM基于健身房管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Java 变量作用域、构造方法官方教程

一、变量作用域 Java 中的变量有3种: &#x1f4d6; ① 全局变量&#xff1a;被定义在类中&#xff08;成员变量&#xff09; &#x1f4d6; ② 局部变量&#xff1a;被定义在成员方法、代码块、静态代码块中定义的变量 &#x1f4d6; ③ 参数&#xff1a;方法声明中的变量 T…

1.Spring简介

1.概念 Spring是一个免费开源框架&#xff0c;为了简化企业级项目开发&#xff0c;提供全面的开发部署解决方案。 2.体系结构 Data Access/Integeration是Spring对数据持久层的支持&#xff0c;SpringDataJpa就是其中的一种。Web是Spring对表现层处理的支持&#xff0c;Spir…

Springboot多环境开发

文章目录一. 前言二. 单文件版多环境配置三. 多文件版多环境配置四. 多环境开发配置技巧五. 多环境配置分组管理一. 前言 在日常开发中我们使用的环境可能会不一样&#xff0c;比如开发环境&#xff0c;测试环境&#xff0c;生产环境&#xff0c;那么这些环境对应的配置也会不…

【计算机视觉】数据获取、数据标注、数据增强的概念简介

觉得有帮助麻烦点赞关注收藏~~~ 基于深度学习的视频分析算法是依赖于数据训练的&#xff0c;数据是深度学习的主要原料&#xff0c;对于算法性能的提升是非常重要的。本章将重点介绍数据的获取、标注、增强及处理方法。 一、数据获取 训练数据的来源主要包括网上公开数据库和…

Redis实现短信登录

文章目录一、基于Session实现登录二、基于Redis实现共享Session实现登录一、基于Session实现登录 ---------------------------------------------------Controller PostMapping("code") public Result sendCode(RequestParam("phone") String phone, Http…

JavaScript进阶教程——面向对象、原型对象、this关键字、bind appl call方法

文章目录面向对象ES2015的面向对象语法&#xff1a;ES5的面向对象语法&#xff1a;原型对象原型链Object对象的原型this关键字**指向调用方法的对象&#xff1a;****构造函数&#xff1a;**触发事件指向全局对象&#xff1a;箭头函数总结bind appl call方法call的用法参数apply…

【计算机图形学入门】笔记4:变换(模型、视图、投影)

目录04变换&#xff08;模型、视图、投影&#xff09;1.使用齐次坐标表示的三维变换2.view/Camera 视图变换3.Projection 投影变换1.Orthographic projection 正交投影2.Perspective projection 透视投影 ——更适合人眼成像&#xff08;近大远小&#xff0c;鸽子为什么这么大&…

AlphaControls 读取存贮数据VCL OnChange事件的处理

AlphaControls 读取存贮数据VCL OnChange事件的处理 AlphaControls控件&#xff0c;窗口时显示&#xff0c;对于VCL控件的处理&#xff0c;需要通过调用皮肤模板重新画出来。因此&#xff0c;在窗口的显示过程中&#xff0c;对VCL控件的数据装载&#xff08;变更&#x…

从获评毕马威中国领先地产科技50强 看贝壳的长期主义

11月9日&#xff0c;在上海进博会现场&#xff0c;毕马威发布了2022年度&#xff08;第二届&#xff09;“毕马威中国领先地产科技企业50”报告&#xff0c;并正式发布了年度毕马威中国领先地产科技企业50榜及地产科技新锐企业榜。作为国内居住产业数字服务平台的贝壳上榜。 众…

前辈给的 Spring Cloud 与 Docker 微服务实战,挽救了要被辞退的我

前言 还记得那天天气阴冷&#xff0c;整个人心都是拔凉拔凉的&#xff1b;原因是因为领导找我聊了一下&#xff0c;委婉地说觉得我的技术不太行&#xff0c;能力不突出&#xff1b;要么自己做出改变&#xff0c;要么选择离开。 说实话&#xff0c;我是有点难过的&#xff0c;…