跳到主要内容Mockingbird C++ Mocking Framework 轻量级元编程解析 | 极客日志C++
Mockingbird C++ Mocking Framework 轻量级元编程解析
Mockingbird 是一个基于 C++ 模板元编程和预处理器生成的轻量级 Mock 框架。它无需额外编译步骤或代码生成工具,仅通过头文件即可工作。核心机制是将被 Mock 的方法转换为可注入行为的函数代理,支持运行时动态替换行为、一次性调用(CallOnce)、调用计数及 Spy 模式。该框架兼容多种测试框架,支持虚函数、重载、const 成员函数及模板类的 Mock,具有低侵入性和跨平台特性。
Mockingbird C++ Mocking Framework
Mockingbird 是一个轻量级、简单且强大的 C++ Mock 框架。
一、核心特性
1. 轻量级
- 仅约 100 行头文件
- 无需额外编译步骤
- 无需代码生成工具
- 无需链接额外库
复杂度 ≈ O(1),而非依赖 libclang + LLVM + 生成器。
2. 基于元编程
Mockingbird 是一个基于元编程(Meta-programming)的框架。它生成的 Mock 类会被增强,使其具备在运行时动态注入方法行为的能力。
C++ 中的元编程通常指:
SFINAEConcepts预处理器抽象表达:Program → Generates Program 或 f(type) → new class。
3. 运行时行为注入
普通函数行为固定。Mockingbird 让方法变成可调用对象(callable object),而非固定函数体。
std::function<bool(std::string)> connect_behavior;
二、最小实现示例
class Database {
public:
bool connect(const std::string& url);
};
#include <functional>
#include <stdexcept>
#include <string>
class MockDatabase {
public:
using ConnectType = std::function<bool(const std::string&)>;
void inject_connect(ConnectType func) {
connect_behavior = std::move(func);
}
bool connect(const std::string& url) {
if (!connect_behavior)
throw std::runtime_error("No behavior injected");
return connect_behavior(url);
}
private:
ConnectType connect_behavior;
};
void test_success_case() {
MockDatabase mock;
mock.inject_connect([](const std::string& url) {
if (url != "db://test")
throw std::runtime_error("Wrong url");
return true;
});
bool result = mock.connect("db://test");
assert(result == true);
}
三、核心机制详解
1. 宏自动生成
使用宏自动生成内部函数容器类、注入接口、调用计数器及函数重写逻辑。
2. 运行时替换函数行为
3. 不依赖任何测试框架
可与 Catch2、Google Test、Microsoft Unit Testing Framework 等结合使用。
四、功能特性
1. Mock virtual methods
如果基类中函数是 virtual,Mock 类可以重写它,从而在运行时多态替换行为。
2. Hide non-virtual ones
即使函数不是 virtual,也可以通过在派生类中重新定义同名函数发生名称隐藏(name hiding)。注意:这种情况不会多态调用,必须通过 Mock 类型调用。
3. Mock overloaded and const methods
支持重载和 const 成员函数。Mockingbird 使用 FUNCTION_OVERLOAD(...) 并通过 Disc 标识符区分。
4. Call Once mechanism
核心思想是维护一个函数列表,每次调用优先取出一次性函数,用完即删除。数学表达:设 C = CallOnce 列表大小,每次调用 C = C - 1,直到 C = 0 之后调用默认函数。
5. Mock class template
template<typename T> class A { public: virtual T get(); };
START_MOCK_TEMPLATE(MockA, A, T)
6. Spy on original methods
默认函数就是原函数实现。如果不注入新行为,实际执行原函数,这就是 Spy 模式。
7. Count number of calls
每次调用 Counter++,可通过 GetFooCallCounter() 获取调用次数。
8. Multiple inheritance mock
支持多继承,宏 START_MOCK(Mock, Base1, Base2) 展开为 class Mock : public Base1, public Base2。
五、工作原理
每个被 mock 的函数都被转换为一个'函数代理系统'。结构如下:
- 内部函数容器类(保存默认函数、CallOnce 列表)
- 调用计数器
- 注入函数接口
- 重写函数
调用流程数学模型
调用函数 f_mock(x, y),CallOnce 列表大小为 C:
f_mock(x, y) =
f_callOnce(x, y), if C > 0
f_default(x, y), if C = 0
六、完整实现代码
以下是 Mockingbird 的核心头文件实现概要:
#pragma once
#ifndef MOCKINGBIRD
#define MOCKINGBIRD
#include <functional>
#include <list>
#define SELECT_MACRO(_1, _2, _3, _4, _5, NAME, ...) NAME
#define PREPEND_EXPR1(expr, x) expr x
#define PREPEND_EXPR2(expr, x1, x2) expr x1, PREPEND_EXPR1(expr, x2)
#define PREPEND_EXPR3(expr, x1, x2, x3) expr x1, PREPEND_EXPR2(expr, x2, x3)
#define PREPEND_EXPR4(expr, x1, x2, x3, x4) expr x1, PREPEND_EXPR3(expr, x2, x3, x4)
#define PREPEND_EXPR5(expr, x1, x2, x3, x4, x5) expr x1, PREPEND_EXPR4(expr, x2, x3, x4, x5)
#define PREPEND_EXPR(expr, ...) SELECT_MACRO(__VA_ARGS__, PREPEND_EXPR5, PREPEND_EXPR4, PREPEND_EXPR3, PREPEND_EXPR2, PREPEND_EXPR1)(expr, __VA_ARGS__)
#define START_MOCK_TEMPLATE(MockingClass, MockedClass, ...) \
template<PREPEND_EXPR(typename, __VA_ARGS__)> \
class MockingClass : public MockedClass<__VA_ARGS__> {
#define FUNCTION_BODY(Counter, Member, ...) \
Counter++; \
if (Member.m_CallOnce.empty()) \
return Member.m_Func(__VA_ARGS__); \
auto lastElem = Member.m_CallOnce.back(); \
Member.m_CallOnce.pop_back(); \
return lastElem(__VA_ARGS__);
#define FUNCTION_INJECTION_SET(FuncName, Substitute) \
private: \
template<class Type> \
class FuncName##Class { \
public: \
FuncName##Class() : m_Func((Type)Substitute) {} \
Type m_Func; \
mutable std::list<Type> m_CallOnce; \
}; \
FuncName##Class<decltype(Substitute)> m_##FuncName##Class; \
mutable unsigned m_##FuncName##CallCounter = 0; \
public: \
void Inject##FuncName(decltype(Substitute) sub) { \
m_##FuncName##Class.m_Func = sub; \
} \
void Inject##FuncName##CallOnce(decltype(Substitute) sub) { \
m_##FuncName##Class.m_CallOnce.push_front(sub); \
} \
int Get##FuncName##CallCounter() { return m_##FuncName##CallCounter; }
#define FUNCTION_OVERLOAD_INJECTION_SET(FuncName, Substitute, Disc) \
private: \
FuncName##Class<decltype(Substitute)> m_##FuncName##Class##Disc; \
mutable unsigned m_##FuncName##Disc##CallCounter = 0; \
public: \
void Inject##FuncName(decltype(Substitute) sub) { \
m_##FuncName##Class##Disc.m_Func = sub; \
} \
void Inject##FuncName##CallOnce(decltype(Substitute) sub) { \
m_##FuncName##Class##Disc.m_CallOnce.push_front(sub); \
} \
int Get##FuncName##Disc##CallCounter() { return m_##FuncName##Disc##CallCounter; }
#define START_MOCK(MockingClass, ...) \
class MockingClass : PREPEND_EXPR(public, __VA_ARGS__) {
#define FUNCTION(FuncName, ReturnType, Signature, Const, Override, ExpressionWithoutCommas, ...) \
private: \
static ReturnType FuncName##Type Signature { ExpressionWithoutCommas; } \
FUNCTION_INJECTION_SET(FuncName, &FuncName##Type) \
ReturnType FuncName Signature Const Override { \
FUNCTION_BODY(m_##FuncName##CallCounter, m_##FuncName##Class, __VA_ARGS__) \
}
#define END_MOCK(MockingClass) \
private: \
std::function<void(void)> m_dtor = []() {}; \
public: \
void InjectDestructor(std::function<void(void)> dtor) { m_dtor = dtor; } \
~MockingClass() { m_dtor(); } \
};
#endif
七、使用示例
PointServiceMock 示例
#include <cassert>
#include <iostream>
struct Point {
int m_X = 0;
int m_Y = 0;
int m_Z = 0;
};
class IPointService {
public:
virtual Point CreatePoint(int x, int y) = 0;
virtual Point CreatePoint(int x, int y, int z) const = 0;
virtual ~IPointService() {}
};
START_MOCK(PointServiceMock, IPointService)
FUNCTION(CreatePoint, Point, (int x, int y), , override, return Point{}, x, y)
FUNCTION_OVERLOAD(CreatePoint, Point, (int x, int y, int z), const, override, return Point{}, 3D, x, y, z)
END_MOCK(PointServiceMock)
int main() {
PointServiceMock pointService;
auto create2DPointShifted = [](int x, int y) -> Point {
return Point{x + 5, y + 5};
};
pointService.InjectCreatePoint(create2DPointShifted);
auto point2D = pointService.CreatePoint(0, 0);
assert(5 == point2D.m_X);
assert(5 == point2D.m_Y);
std::cout << "2D CreatePoint called: " << pointService.GetCreatePointCallCounter() << " times\n";
return 0;
}
八、总结
Mockingbird 的本质是使用宏自动生成一个函数代理调度器。每个函数变成:
外部调用 ↓ Mock override ↓ 计数++ ↓ CallOnce? ↓ 执行对应函数
它利用 C++ 模板、std::function 和预处理器实现运行时行为替换,而不是编译期函数重写。该框架具有低侵入性、跨平台、无外部依赖的特点,适合与各种测试框架结合使用。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online