传统测试中的Mock, 都是基于多态实现的, 也就是Mock面向接口的虚函数. 但是在C++的代码中, 经常会混入大量的C函数或是静态成员函数.
例如工厂函数, 单例函数, 或是C库中的函数甚至STL的算法等.

对于这些静态函数, 比较传统的做法是创建一个Wrapper, 用虚方法对这些静态函数进行包裹. 在测试的时候对Wrapper进行Mock便可控制被包裹的静态函数的行为:

1
int add(int x, int y);

可以通过Wrapper包裹为:

1
2
3
4
5
6
class Calc {
public:
virtual int add(int x, int y) {
return ::add(x, y);
}
}

但是对于存量代码, 这种重构并不现实(工作量及流程问题). 正当我们束手无策时, 我们发现mockcpp可以帮助我们解决一部分静态方法Mock的需求.

Mock静态函数

虽然mockcpp主要提供面向虚方法的Mock, 但是mockcpp同时通过inline hooking提供了对静态方法的Mock, 更为强大的是它的inline hooking支持32位和64位的Windows及Linux环境.

抛开实现, 我们来看两个mockcpp的应用实例:

1
2
3
4
5
6
7
8
9
10
11
12
// Mock C Function
static int add(int x, int y) {
return x + y;
}
TEST(Mockcpp, ShouldAbleToMockCFunction) {
MOCKER(add)
.expects(once())
.with(eq(1), eq(2))
.will(returnValue(0));
ASSERT_EQ(0, add(1, 2));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Mock Static Member Function
class Calc {
class Calc {
public:
static int add(int x, int y) {
return x + y;
}
};
TEST(Mockcpp, ShouldAbleToMockStaticFunction) {
MOCKER(&Calc::add)
.expects(once())
.with(eq(1), eq(2))
.will(returnValue(0));
ASSERT_EQ(0, Calc::add(1, 2));
}

mockcpp通过MOCKER宏改变了C函数add及静态成员函数Calc::add的行为. 并且对这两个函数的调用设置了期望.
对于mockcpp的详细用法可以查看这个文档: mockcpp使用方法简明指导

Mockcpp对静态函数的副作用

前面提到, mockcpp对静态函数的Mock是通过inline hooking实现的. 我们来简单看下这个inline hook的过程.

首先是int add(int, int)在x64下生成的汇编指令:

1
2
3
4
5
6
7
8
9
10
11
Dump of assembler code for function _ZL3addii:
0x00000000004060a4 <+0>: push %rbp
0x00000000004060a5 <+1>: mov %rsp,%rbp
0x00000000004060a8 <+4>: mov %edi,-0x4(%rbp)
0x00000000004060ab <+7>: mov %esi,-0x8(%rbp)
0x00000000004060ae <+10>: mov -0x8(%rbp),%eax
0x00000000004060b1 <+13>: mov -0x4(%rbp),%edx
0x00000000004060b4 <+16>: add %edx,%eax
0x00000000004060b6 <+18>: pop %rbp
0x00000000004060b7 <+19>: retq
End of assembler dump.

通过mockcpp Mock之后, 开头的一段指令让逻辑跳转到了<_ZN38Mockcpp_ShouldAbleToMockCFunction_Test8TestBodyEv+53>:

1
2
3
4
5
6
7
8
9
10
11
Dump of assembler code for function _ZL3addii:
0x00000000004060a4 <+0>: jmpq *0x0(%rip) # 0x4060aa <_ZL3addii+6>
0x00000000004060aa <+6>: sahf
0x00000000004060ab <+7>: js 0x4060ed <_ZN38Mockcpp_ShouldAbleToMockCFunction_Test8TestBodyEv+53>
0x00000000004060ad <+9>: add %al,(%rax)
0x00000000004060af <+11>: add %al,(%rax)
0x00000000004060b1 <+13>: add %dl,-0x4(%rbp)
0x00000000004060b4 <+16>: add %edx,%eax
0x00000000004060b6 <+18>: pop %rbp
0x00000000004060b7 <+19>: retq
End of assembler dump.

而跳转到的<_ZN38Mockcpp_ShouldAbleToMockCFunction_Test8TestBodyEv+53>正是我们通过mockcpp设置的expects:

1
2
3
4
5
6
7
Dump of assembler code for function _ZN38Mockcpp_ShouldAbleToMockCFunction_Test8TestBodyEv:
...
0x00000000004060ed <+53>: lea -0x60(%rbp),%rax
0x00000000004060f1 <+57>: mov %rax,%rdi
0x00000000004060f4 <+60>: callq 0x449396 <_ZN7mockcpp11returnValueERKNS_3AnyE>
...
End of assembler dump.

然后问题来了, mockcpp的inline hooking只会对原函数做一次, 如果发现原来的函数已经被hook过, 就会忽略后续的MOCKER.
这会导致:

  • 被Mock的函数在脱离MOCKER作用域后依旧生效
  • 被Mock的函数后续不能继续通过MOCKER更改行为

对于第一个问题, 暂时没有发现什么比较好的方法, mockcpp在做inline hooking的时候已经破坏了函数入口的汇编逻辑.

而对于第二种情况, 我么则可以通过一种proxy的方法, 通过mockcpp将静态函数转发到某个被Mock的成员函数上, 然后再通过控制这个成员函数改变原来静态函数的行为.
具体的实现可以参考下面一节的示例.

使用Google Mock语法Mock静态函数

mockcpp虽然能够对静态函数进行Mock, 但mockcpp出现的年代比较久远, 限于当时的编译器能力, 语法不是很友好.

在目前的环境中, Google Mock无疑是一个更好的选择(编译器允许的话也可以尝试FakeIt). 但是Google Mock并不具备对静态方法进行Mock的能力.

通过mockcpp做为proxy, 我们可以在不学习新的mockcpp语法的情况下对静态函数进行Mock(同时解决了mockcpp不能多次Mock同一个静态函数的问题).

我们先来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Calc {
public:
static int add(int x, int y) {
return x + y;
}
};
class CalcMock {
public:
static CalcMock &getInstance() {
static CalcMock calcMock;
return calcMock;
}
MOCK_METHOD2(add, int(int, int));
static int addProxy(int x, int y) {
return getInstance().add(x, y);
}
};
TEST(Mockcpp, ShouldAbleToMockStaticFunctionWithGoogleMock) {
MOCKER(Calc::add)
.defaults()
.with(any(), any())
.will(invoke(CalcMock::addProxy));
EXPECT_CALL(CalcMock::getInstance(), add(1, 2))
.WillRepeatedly(testing::Return(0));
ASSERT_EQ(0, Calc::add(1, 2));
EXPECT_CALL(CalcMock::getInstance(), add(1, 2))
.WillRepeatedly(testing::Return(10));
ASSERT_EQ(10, Calc::add(1, 2));
}

我们在Mock Calc::add方法时, 首先使用Mockcpp将所有Calc:add的调用转发到CalcMock::addProxy上.
之后在CalcMock::addProxy里则会调用CalcMock单例的CalcMock::add方法, 而这个CalcMock::add方法则是通过Google Mock声明的.
可以使用EXPECT_CALL及其他Google Mock支持的语法对Mock的Calc::add设置期望.

此时我们已经可以脱离Mockcpp, 使用更加友好的Google Mock来控制静态函数.

在这个示例中, 我们同时解决了上一节提到的Mockcpp不能多次对同一静态函数Mock的问题.
我们使用Mockcpp一劳永逸的将静态函数转发给了Google Mock, 而Google Mock则可以对Mock对象的单例任意设置期望.