文章目录
- 0.概要
- 1. 为什么要封装GMock?
- 2. `stub_mock.h` 的设计与实现
- 2.1 接口(宏)介绍
- 2.2 核心实现细节
- 2.3 使用示例
- 2.3.1 静态函数打桩
- 2.3.2 类成员函数打桩
- 2.3.3 虚函数打桩
- 2.3.4 重载函数打桩
- 2.4 lambda表达式的使用场景
- 2.5 gmock action
- 3 `stub.h` 的介绍
- 3.1 替换函数实现
- 3.2 内存保护机制
- 4 结论
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的不足,我们设计并实现了StubMock
。StubMock
结合了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 lambdaNF_SMOCK(0, read, [] { return 0; }); // have return typeNF_SMOCK(1, read, [] {}); // no return typeNF_SMOCK(2, ADDR(CLS, s1), [] {}); // class static function// use gmock actionNF_SMOCK(0, read, Return(0)); // have return typeNF_SMOCK(1, read, Return()); // no return typeNF_SMOCK(2, ADDR(CLS, s1), Return()); // class static function// do somethingSMOCK_CLEAR; // clear
}
2.3.2 类成员函数打桩
using namespace testing;class CLS {int cfn(int x) const { return 0; }
};TEST {// use lambdaNF_SMOCK(0, ADDR(CLS, cfn), [] { return 1; });// use gmock actionNF_SMOCK(0, ADDR(CLS, cfn), Return(1));// do somethingSMOCK_CLEAR; // clear
}
2.3.3 虚函数打桩
using namespace testing;class CLS {virtual int vir_fun() const { return 0; }
};TEST {// use lambdaNF_SMOCK(0, V_ADDR(CLS, vir_fun), [] { return 1; });// use gmock actionNF_SMOCK(0, V_ADDR(CLS, vir_fun), Return(1));// do somethingSMOCK_CLEAR; // clear
}
2.3.4 重载函数打桩
class CLS {int fun() const { return 0; }int fun(double) const { return 0; }
};TEST(a, b) {// use lambdaNF_SMOCK(0, O_ADDR(CLS, fun, int, (), (const)), [] { return 1; });NF_SMOCK(1, O_ADDR(CLS, fun, int, (double), (const)), [] { return 2; });// use gmock actionNF_SMOCK(0, O_ADDR(CLS, fun, int, (), (const)), Return(1));NF_SMOCK(1, O_ADDR(CLS, fun, int, (double), (const)), Return(2));// do somethingSMOCK_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 somethingSMOCK_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 somethingSMOCK_CLEAR; // 清除存根
}
3 stub.h
的介绍
stub.h
是来自第三方库 coolxv/cpp-stub
的内容,该库提供了一种替换函数实现的方法,允许我们在运行时动态地替换函数的实现。stub.h
的主要功能是通过直接修改内存中的函数代码,实现函数的替换和恢复。以下是其主要特性:
- 跨平台支持:支持Windows和Linux操作系统,兼容多种CPU架构(如x86、ARM、MIPS、RISC-V等)。
- 高效的指令缓存刷新:根据平台的不同,使用适当的方法刷新指令缓存,确保函数替换后的代码能立即生效。
- 灵活的函数替换:使用不同的替换策略(近跳转和远跳转),根据具体情况选择合适的方式来替换函数。
- 内存保护机制:在修改函数代码前后,修改内存保护属性以确保安全性和正确性,防止非法内存访问导致的崩溃。
- 自动管理和恢复:维护一个
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 _WIN32DWORD lpflOldProtect;if (0 != VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READWRITE, &lpflOldProtect))
#elseif (0 == mprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_WRITE | PROT_EXEC))
#endif{// 修改函数代码// 恢复内存保护
#ifdef _WIN32VirtualProtect(pageof(pstub->fn), m_pagesize * 2, PAGE_EXECUTE_READ, &lpflOldProtect);
#elsemprotect(pageof(pstub->fn), m_pagesize * 2, PROT_READ | PROT_EXEC);
#endif}
4 结论
通过结合stub.h和gmock
进行封装,stub_mock.h
提供了一个更加灵活、强大和高效的模拟框架。它不仅扩展了GMock的功能,使其能够支持更多的场景,还简化了测试代码的编写和管理。