请考虑以下代码:
class SomeInterface
{
public:
virtual void foo() = 0;
virtual ~SomeInterface() {}
};
class RealImplementation : public SomeInterface
{
public:
void foo() { /* do complete stuff */}
};
class MockImplementation : public SomeInterface
{
public:
void foo() { /* do simple stuff */ }
};
更具体的是有一些例子:
class IInjector
{
public:
virtual bool injectDLL() const = 0;
virtual ~IInjector() {}
};
class RealInjector : public IInjector
{
public:
bool injectDLL() const
{
int pid = GetHookedProcessId();
char name = readDllNameFromSomewhere();
if (loadDllInSomeProcess(pid, name))
return true;
else
return false;
}
};
class Hook
{
public:
bool hookProcess(const IInjector& injector)
{
return injector.injectDLL();
}
};
然后在测试代码中,通常的情况是这样做:
class MockInjector : public IInjector {
public:
MOCK_METHOD0(injectDLL, bool());
};
TEST(HookTest, CanHookSomething) {
MockInjector injector;
EXPECT_CALL(injector, injectDLL()).Times(1);
Hook hook;
EXPECT_TRUE(hook.hookProcess(injector));
}
为了验证injectDLL
方法被调用,我们必须知道hookProcess
称为injectDLL
。但它是方法hookProcess
的实现细节。因此,在我们的测试中,我们打开一些实现细节。但这是很常见的情况。那么,使用模拟时可以打开一些实现细节吗?
单元测试通常用作白盒测试技术 - 您知道测试中的代码。否则,您也无法对测试实现的代码覆盖率做出声明。并且,单元测试代码被认为属于代码,与代码等一起置于版本控制之下。因此,对于您的单元测试,您不会“披露”实现细节 - 它们不会被隐藏。
然而,当然,通过模拟,您的测试变得更依赖于实现细节,这意味着它们更有可能破坏或需要维护。但这只是在这种情况下你可能不得不接受的权衡。
如果Hook::injectProcess
的函数规范是使用用户提供的IInjector
实例来注入DLL,那么任何不调用该用户提供的实例上的injectProcess
方法的injectDLL
实现肯定会被破坏。因此,在您提供的示例案例中,似乎根本没有中断封装。
您展示的示例是依赖注入的一个很好的示例,非常适合使用Google Test等框架进行测试。它允许通过使用用户可构造的模拟对象调用公共函数来模拟在公共接口中形式化的某些特定操作。
但是,在一般情况下,这样的代码可能会破坏封装。考虑被测试的函数不是hookProcess
方法,其明确指定的工作是注入DLL,因此您确定必须注入DLL。您可能想要测试应该获取某些数据的函数,并且当前需要将一些DLL注入某些进程来执行此操作,但是可能还有其他方法来获取不涉及DLL注入或将不同DLL注入不同进程的信息。在这种情况下,测试哪些DLL被注入哪里会破坏“只是获取信息”规范所提供的封装。 Dirk Herrmann的回答讨论了这个案例。