探讨GMock封装与StubMock的实现及其优点

0.概要

在软件开发和测试过程中,模拟(mocking)是一个非常重要的技术手段。特别是在单元测试中,模拟对象可以帮助我们隔离被测试的代码,确保测试的独立性和准确性。Google Mock(GMock)是一个广泛使用的C++模拟框架,但在一些复杂的场景下,直接使用GMock可能会显得不够灵活和高效。本文将深入探讨如何通过封装GMock,结合第三方库cpp-stub中的stub.h,实现一个功能更强大且灵活的模拟框架——StubMock。

1. 为什么要封装GMock?

虽然GMock已经提供了强大的模拟功能,但在某些特定场景下,直接使用GMock仍然存在一些不足:

  • 复杂的设置和管理

    • 在大型项目中,涉及多个类和函数的复杂交互时,直接使用GMock进行设置和管理可能变得繁琐和复杂。封装可以简化这些设置过程,使代码更清晰和易于维护。
  • 类型安全和编译期检查

    • 尽管GMock提供了良好的类型安全支持,但封装可以进一步强化这一点。例如,使用模板和宏定义,可以在编译期进行更严格的类型检查,避免运行时错误。
  • 统一的管理和复用

    • 对于需要在多个测试用例中反复使用的模拟和存根函数,封装可以提供统一的管理方式。通过单例模式和静态成员变量,可以方便地复用和管理这些函数,减少代码重复。
  • 特殊场景支持

    • 原始GMock可能对某些特殊场景(如特定修饰符的成员函数)支持不够完善。通过封装,可以扩展GMock的功能,支持更多类型的成员函数签名。
  • 简化测试代码

    • 提供了一系列辅助宏,简化测试代码的编写。减少样板代码的数量,使测试代码更简洁、更易读。

2. stub_mock.h 的设计与实现

为了弥补GMock的不足,我们设计并实现了StubMockStubMock结合了GMock和第三方库cpp-stub中的stub.h,为静态函数、类成员函数、虚函数以及重载函数提供了灵活的模拟支持。

代码
橘色的喵/custom_gtest_stub

2.1 接口(宏)介绍

  • NF_SMOCK(n, fn, fn_stub):用于设置静态函数或类静态成员函数的存根。n表示存根函数的编号,fn表示要存根的函数,fn_stub表示用于替换的存根函数或行为。
  • F_SMOCK(fn, fn_stub):类似于NF_SMOCK,但编号由__COUNTER__宏自动生成,避免手动编号。
  • SMOCK_CLEAR:清除所有设置的存根函数,还原原始函数的行为。
  • ADDR(CLASS_NAME, MEMBER_NAME):获取类静态成员函数的地址。
  • V_ADDR(CLASS_NAME, MEMBER_NAME):获取虚函数的地址。
  • O_ADDR(CLASS_NAME, MEMBER_NAME, RETURN, ARGS, SPEC):获取重载函数的地址,需提供函数返回类型、参数列表及修饰符。

2.2 核心实现细节

  • 单例模式

    static StubMock &get_instance() {
        static StubMock stub_mock;
        return stub_mock;
    }
    
    • 确保整个程序只有一个StubMock实例,使用static局部变量实现线程安全的单例模式。
  • 静态存根函数模板类

    template <int N, typename R, typename... ARGS>
    class FnStatic {
    public:
        static testing::Action<R(ARGS...)> action;
    };
    
    • 使用模板类管理静态存根函数,每个函数都有一个唯一的编号N,确保不同函数的独立管理。
  • 设置存根函数

    template <int N = 0, typename F, typename R, typename... ARGS>
    static void set_fn(R (*fn)(ARGS...), F fn_stub) {
        FnStatic<N, R, ARGS...>::action = fn_stub;
        get_instance().set(fn, &StubMock::call_fn<N, R, ARGS...>);
    }
    
    • 通过模板参数设置存根函数,并将其与StubMock::call_fn绑定,确保调用时能正确执行存根函数。
  • 辅助宏

    #define F_SMOCK(fn, fn_stub) NF_SMOCK(__COUNTER__, fn, fn_stub)
    
    • 使用宏简化存根函数的设置,__COUNTER__宏自动生成唯一编号,减少手动管理的复杂性。
  • 成员函数地址获取

    #define V_ADDR(CLASS_NAME, MEMBER_NAME) decltype(StubMock::vfn_addr(&CLASS_NAME::MEMBER_NAME))(&CLASS_NAME::MEMBER_NAME)
    
    • 使用decltype和模板函数,安全地获取成员函数地址,确保类型匹配。

2.3 使用示例

以下是一些使用StubMock进行函数打桩的示例:

2.3.1 静态函数打桩

// unistd.h
extern ssize_t read(int __fd, void *__buf, size_t __nbytes) __wur;
// string.h
extern void *memset(void *__s, int __c, size_t __n) __THROW __nonnull((1));

using namespace testing;

class CLS {
public:
  static void s1() {}
};

TEST {
  // use lambda
  NF_SMOCK(0, read, [] { return 0; }); // have return type
  NF_SMOCK(1, read, [] {}); // no return type
  NF_SMOCK(2, ADDR(CLS, s1), [] {}); // class static function
  
  // use gmock action
  NF_SMOCK(0, read, Return(0)); // have return type
  NF_SMOCK(1, read, Return()); // no return type
  NF_SMOCK(2, ADDR(CLS, s1), Return()); // class static function
  
  // do something
  
  SMOCK_CLEAR; // clear
}

2.3.2 类成员函数打桩

using namespace testing;

class CLS {
  int cfn(int x) const { return 0; }
};

TEST {
  // use lambda
  NF_SMOCK(0, ADDR(CLS, cfn), [] { return 1; });
  
  // use gmock action
  NF_SMOCK(0, ADDR(CLS, cfn), Return(1));
  
  // do something
  
  SMOCK_CLEAR; // clear
}

2.3.3 虚函数打桩

using namespace testing;

class CLS {
  virtual int vir_fun() const { return 0; }
};

TEST {
  // use lambda
  NF_SMOCK(0, V_ADDR(CLS, vir_fun), [] { return 1; });
  
  // use gmock action
  NF_SMOCK(0, V_ADDR(CLS, vir_fun), Return(1));
  
  // do something
  
  SMOCK_CLEAR; // clear
}

2.3.4 重载函数打桩

class CLS {
  int fun() const { return 0; }
  int fun(double) const { return 0; }
};

TEST(a, b) {
  // use lambda
  NF_SMOCK(0, O_ADDR(CLS, fun, int, (), (const)), [] { return 1; });
  NF_SMOCK(1, O_ADDR(CLS, fun, int, (double), (const)), [] { return 2; });
  
  // use gmock action
  NF_SMOCK(0, O_ADDR(CLS, fun, int, (), (const)), Return(1));
  NF_SMOCK(1, O_ADDR(CLS, fun, int, (double), (const)), Return(2));
  
  // do something
  
  SMOCK_CLEAR; // clear
}

2.4 lambda表达式的使用场景

lambda表达式通常用于逻辑复杂的场景,例如根据条件返回不同的值:

// unistd.h
extern ssize_t read(int __fd, void *__buf, size_t __nbytes) __wur;

TEST {
  NF_SMOCK(0, read, [] {
    static int cnt = 0;
    cnt++;
    if (cnt == 1) return 0;
    return -1;
  });
  
  // do something
  
  SMOCK_CLEAR; // clear
}

2.5 gmock action

gmock中最常用的action是Return函数,用于指定模拟函数的返回值。例如:

using namespace testing;

class CLS {
  static int sfn() { return 0; }
};

TEST {
  NF_SMOCK(0, ADDR(CLS, sfn), Return(1)); // 设置静态函数的存

根,返回1
  // do something
  SMOCK_CLEAR; // 清除存根
}

3 stub.h 的介绍

stub.h 是来自第三方库 coolxv/cpp-stub 的内容,该库提供了一种替换函数实现的方法,允许我们在运行时动态地替换函数的实现。stub.h 的主要功能是通过直接修改内存中的函数代码,实现函数的替换和恢复。以下是其主要特性:

  1. 跨平台支持:支持Windows和Linux操作系统,兼容多种CPU架构(如x86、ARM、MIPS、RISC-V等)。
  2. 高效的指令缓存刷新:根据平台的不同,使用适当的方法刷新指令缓存,确保函数替换后的代码能立即生效。
  3. 灵活的函数替换:使用不同的替换策略(近跳转和远跳转),根据具体情况选择合适的方式来替换函数。
  4. 内存保护机制:在修改函数代码前后,修改内存保护属性以确保安全性和正确性,防止非法内存访问导致的崩溃。
  5. 自动管理和恢复:维护一个std::map来记录所有被替换的函数信息,支持函数的自动恢复和清理。

以下是stub.h的一些关键实现细节:

3.1 替换函数实现

根据不同的CPU架构,选择合适的替换策略:

// x86_64架构
#define REPLACE_FAR(t, fn, fn_stub)            \
  *fn = 0x49;                                  \
  *(fn + 1) = 0xbb;                            \
  *(long long *)(fn + 2) = (long long)fn_stub; \
  *(fn + 10) = 0x41;                           \
  *(fn + 11) = 0xff;                           \
  *(fn + 12) = 0xe3;                           \
  CACHEFLUSH((char *)fn, CODESIZE);

// 5 byte(jmp rel32)
#define REPLACE_NEAR(t, fn, fn_stub)                     \
  *fn = 0xE9;                                            \
  *(int *)(fn + 1) = (int)(fn_stub - fn - CODESIZE_MIN); \
  CACHEFLUSH((char *)fn, CODESIZE);

3.2 内存保护机制

在修改函数代码前后,修改内存保护属性:

#ifdef _WIN32
  DWORD lpflOldProtect;
  if (0 != VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect))
#else
  if (0 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC))
#endif
  {
    // 修改函数代码
    // 恢复内存保护
#ifdef _WIN32
    VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect);
#else
    mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_EXEC);
#endif
  }

4 结论

通过结合stub.hgmock进行封装,stub_mock.h提供了一个更加灵活、强大和高效的模拟框架。它不仅扩展了GMock的功能,使其能够支持更多的场景,还简化了测试代码的编写和管理。

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值