不,您不需要求助于模拟静态类 - 这是众多选择之一。
如果您在嵌入式环境中虚拟调度开销太大,或者该架构的编译器/链接器优化器做得非常糟糕,那么您可以尝试以下 3 种方法来模拟平台调用。
为简单起见,假设您要模拟 std::this_thread 命名空间中的函数,例如 sleep_for(std::milliseconds)。
示例 0 - 无法测试的基线
不进行模拟,假设您的代码如下所示:
class untestable_class
{
public:
void some_function()
{
if (must_sleep())
{
auto sleep_duration = std::chrono::milliseconds(1000);
std::this_thread::sleep_for(sleep_duration);
}
}
};
你会像这样使用那个类:
void use_untestable_class()
{
untestable_class instance;
instance.some_function();
}
由于对标准库 sleep_for 函数的依赖,你有一个平台依赖,这使得 some_function 很难在没有实际进行集成测试的情况下进行单元测试。
示例 1 - 使用静态策略可测试
通过使用类模板告诉我们的类使用特定的线程策略,我们可以抽象出单元测试中的平台依赖性。该策略可以是静态的,也可以是实例的——它们都消除了在运行时对虚拟调度的需求,而且它们很容易被编译器/链接器优化。
在静态策略案例中,我们有一个依赖于平台的“真实”策略:
struct system_thread_policy1
{
static void sleep_milliseconds(long milliseconds)
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}
};
我们还有一个可以在单元测试中控制的“模拟”策略:
struct mock_thread_policy1
{
// Mock attributes to verify interactions.
static size_t sleep_milliseconds_count;
static size_t sleep_milliseconds_arg1;
// Resets the mock attributes before usage.
static void sleep_milliseconds_reset()
{
sleep_milliseconds_count = 0;
sleep_milliseconds_arg1 = 0;
}
static void sleep_milliseconds(size_t milliseconds)
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};
// This is needed with MS compilers to keep all mock code in a header file.
__declspec(selectany) size_t mock_thread_policy1::sleep_milliseconds_count;
__declspec(selectany) size_t mock_thread_policy1::sleep_milliseconds_arg1;
使用策略的生产类将策略类型作为模板参数并静态调用其sleep_milliseconds:
template <typename thread_policy>
class testable_class1
{
public:
void some_function()
{
if (must_sleep())
{
thread_policy::sleep_milliseconds(sleep_duration_milliseconds);
}
}
private:
enum { sleep_duration_milliseconds = 1000 };
};
在生产代码中,testable_class1 使用“真实”策略进行实例化:
void use_testable_class1()
{
testable_class1<system_thread_policy1> instance;
instance.some_function();
}
在单元测试中,testable_class1 使用“模拟”策略进行实例化:
void test_testable_class1()
{
mock_thread_policy1::sleep_milliseconds_reset();
testable_class1<mock_thread_policy1> instance;
instance.some_function();
assert(mock_thread_policy1::sleep_milliseconds_count == 1);
assert(mock_thread_policy1::sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on instance");
}
这种方法的好处:
- 测试交互的功能,例如上面的调用计数和参数检查,可以添加到模拟中并用于验证类交互单元测试。李>
- 静态调用使优化器很容易将“真实”调用内联到
sleep_for。
这种方法的缺点:
- 静态状态会为模拟添加噪音。
- 需要在每个使用它的单元测试中重置静态状态,因为不同的单元测试会改变该粘性状态。
- 如果测试运行程序并行化单元测试,静态状态会导致无法可靠地使用模拟,因为不同的线程会处理相同的状态,从而导致不可预测的行为。
示例 2 - 使用实例策略可测试
在 instance 策略案例中,我们有一个依赖于平台的“真实”策略:
struct system_thread_policy2
{
void sleep_milliseconds(size_t milliseconds) const
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}
};
我们还有一个可以在单元测试中控制的“模拟”策略:
struct mock_thread_policy2
{
mutable size_t sleep_milliseconds_count;
mutable size_t sleep_milliseconds_arg1;
mock_thread_policy2()
: sleep_milliseconds_count(0)
, sleep_milliseconds_arg1(0)
{
}
void sleep_milliseconds(size_t milliseconds) const
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};
使用策略的生产类将策略类型作为模板参数,获取注入到构造器中的策略实例并调用其sleep_milliseconds:
template <typename thread_policy>
class testable_class2
{
public:
testable_class2(const thread_policy& policy = thread_policy()) : m_thread_policy(policy) { }
void some_function() const
{
if (must_sleep())
{
m_thread_policy.sleep_milliseconds(sleep_duration_milliseconds);
}
}
private:
// Needed since the thread policy is taken as a reference.
testable_class2(const testable_class2&);
testable_class2& operator=(const testable_class2&);
enum { sleep_duration_milliseconds = 1000 };
const thread_policy& m_thread_policy;
};
在生产代码中,testable_class2 使用“真实”策略进行实例化:
void use_testable_class2()
{
const testable_class2<system_thread_policy2> instance;
instance.some_function();
}
在单元测试中,testable_class2 使用“模拟”策略进行实例化:
void test_testable_class2()
{
mock_thread_policy2 thread_policy;
const testable_class2<mock_thread_policy2> instance(thread_policy);
instance.some_function();
assert(thread_policy.sleep_milliseconds_count == 1);
assert(thread_policy.sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on instance");
}
这种方法的好处:
- 测试交互的功能,例如上面的调用计数和参数检查,可以添加到模拟中并用于验证类交互单元测试。李>
- 实例调用使优化器很容易将“真实”调用内联到
sleep_for。
- 模拟中没有静态,这使得编写、阅读和维护单元测试更容易。
这种方法的缺点:
- 实例状态为模拟添加了可变噪声。
- 实例状态向客户端添加噪音 (
testable_class2) - 如果交互不需要验证,则可以在构造函数中按值传递策略,并且大多数类 goo 都会消失。
示例 3 - 可使用虚拟策略进行测试
这与前两个示例的不同之处在于它依赖于虚拟分派,但如果编译器/链接器可以检测到所操作的实例是基本类型,则它可能会优化虚拟分派。
首先,我们有一个生产类base,它在非纯虚函数中使用“真实”策略:
class testable_class3
{
public:
void some_function()
{
if (must_sleep())
{
sleep_milliseconds(sleep_duration_milliseconds);
}
}
private:
virtual void sleep_milliseconds(size_t milliseconds)
{
auto sleep_duration = std::chrono::milliseconds(milliseconds);
std::this_thread::sleep_for(sleep_duration);
}
enum { sleep_duration_milliseconds = 1000 };
};
其次,我们有派生的类,它在虚函数中实现了“模拟”策略(一种模板方法设计模式):
class mock_testable_class3 : public testable_class3
{
public:
size_t sleep_milliseconds_count;
size_t sleep_milliseconds_arg1;
mock_testable_class3()
: sleep_milliseconds_count(0)
, sleep_milliseconds_arg1(0)
{
}
private:
virtual void sleep_milliseconds(size_t milliseconds)
{
sleep_milliseconds_count++;
sleep_milliseconds_arg1 = milliseconds;
}
};
在生产代码中,testable_class3 只是作为自身实例化:
void use_testable_class3()
{
// Lots of opportunities to optimize away the virtual dispatch.
testable_class3 instance;
instance.some_function();
}
在单元测试中,testable_class3 使用“模拟”派生类进行实例化:
void test_testable_class3()
{
mock_testable_class3 mock_instance;
auto test_function = [](testable_class3& instance) { instance.some_function(); };
test_function(mock_instance);
assert(mock_instance.sleep_milliseconds_count == 1);
assert(mock_instance.sleep_milliseconds_arg1 == 1000);
//assert("some observable behavior on mock_instance");
}
这种方法的好处:
- 测试交互的功能,例如上面的调用计数和参数检查,可以添加到模拟中并用于验证类交互单元测试。李>
- 对“自身”的基类虚拟调用使优化器可以将“真实”调用内联到
sleep_for。
- 模拟中没有静态,这使得编写、阅读和维护单元测试更容易。
这种方法的缺点:
- 不能将基类标记为
final (C++11),因为它必须允许继承,如果比上面的简单示例更复杂,这可能会影响类设计的其余部分。
- 编译器/链接器可能低于标准或根本无法优化虚拟调度。
测试运行
以上都可以用这个来测试:
int _tmain(int argc, _TCHAR* argv[])
{
test_testable_class1();
test_testable_class2();
test_testable_class3();
return 0;
}
完整的可运行示例位于http://pastebin.com/0qJaQVcD