不,您不需要诉诸模拟静态类 - 这是众多选项之一。
如果您处于虚拟调度开销太大的嵌入式环境中,或者该架构的编译器/链接器优化器做得非常糟糕,那么您可以尝试以下 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 - 可使用静态策略进行测试
通过告诉我们的班级使用特定的线程策略使用类模板,我们可以抽象出单元测试中的平台依赖性。该策略可以是静态的,也可以是实例的 - 它们都消除了运行时虚拟调度的需要,并且编译器/链接器很容易优化。
In the static政策案例,我们有一项取决于平台的“真实”政策:
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
使用“mock”策略实例化:
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");
}
这种方法的优点:
- 测试交互的功能,例如呼叫计数 and 参数检查上面的内容可以添加到模拟中并用于验证类交互单元测试。
- 静态调用使优化器很容易内联“真实”调用
sleep_for
.
这种方法的缺点:
- 静态会给模拟增加噪音。
- 静态状态需要在使用它的每个单元测试中重置,因为不同的单元测试会改变该粘性状态。
- 如果测试运行器并行化单元测试,则静态状态使得无法可靠地使用模拟,因为不同的线程将摆弄相同的状态,从而导致不可预测的行为。
示例 2 - 可使用实例策略进行测试
In the 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
使用“mock”策略实例化:
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");
}
这种方法的优点:
- 测试交互的功能,例如呼叫计数 and 参数检查上面的内容可以添加到模拟中并用于验证类交互单元测试。
- The instance call makes it very easy for the optimizer to inline the "real" call to
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 };
};
其次,我们有derived在虚拟函数中实现“模拟”策略的类(一种模板方法设计模式):
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");
}
这种方法的优点:
- 测试交互的功能,例如呼叫计数 and 参数检查上面的内容可以添加到模拟中并用于验证类交互单元测试。
- 对“自身”的基类虚拟调用使优化器可以内联“真实”调用
sleep_for
.
- 模拟中没有静态,这使得编写、阅读和维护单元测试变得更加容易。
这种方法的缺点:
- 基类不能被标记
final
(C++11),因为必须允许继承它,如果比上面的简单示例更复杂,这可能会影响类设计的其余部分。
- 编译器/链接器可能不合格或者根本无法优化虚拟调度。
Test Run
以上所有内容都可以用以下方法进行测试:
int _tmain(int argc, _TCHAR* argv[])
{
test_testable_class1();
test_testable_class2();
test_testable_class3();
return 0;
}
完整的可运行示例位于http://pastebin.com/0qJaQVcD http://pastebin.com/0qJaQVcD