使用Google Mock进行C ++高性能单元测试?

Zak*_*Zak 5 c++ unit-testing interface static-classes googlemock

我正在使用Google Mock,并且正在努力模拟C ++系统调用(特别是C ++ 11计时函数)。

我知道我应该创建一个接口,创建一个类以为我的实际实现实现该接口,然后在测试中模拟出该接口。我正在尝试编写一个嵌入式应用程序,所以这种间接听起来对我来说太昂贵了。

将系统调用并入Google Mock的最有效/最有效的方法是什么?

Joh*_*ell 5

不,您不必求助于模拟静态类-这是许多选择之一。

如果您处于虚拟分配过多的嵌入式环境中,或者该体系结构的编译器/链接器优化器确实做得很糟糕,则可以尝试以下三种方式来模拟平台调用。

为简单起见,假设您要模拟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);
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

您将像这样使用该类:

void use_untestable_class()
{
    untestable_class instance;
    instance.some_function();
}
Run Code Online (Sandbox Code Playgroud)

由于依赖于标准库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);
    }
};
Run Code Online (Sandbox Code Playgroud)

我们还有一个可以在单元测试中控制的“模拟”策略:

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;
Run Code Online (Sandbox Code Playgroud)

使用该策略的生产类将策略类型作为模板参数,并对其进行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 };
};
Run Code Online (Sandbox Code Playgroud)

在生产代码中,testable_class1使用“真实”策略实例化:

void use_testable_class1()
{
    testable_class1<system_thread_policy1> instance;
    instance.some_function();
}
Run Code Online (Sandbox Code Playgroud)

在单元测试中,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");
}
Run Code Online (Sandbox Code Playgroud)

这种方法的优点:

  • 可以将测试交互的功能(例如上面的调用计数参数检查)添加到模拟中,并用于验证类交互单元测试。
  • 静态调用使优化程序很容易内联到的“实际”调用sleep_for

这种方法的缺点:

  • 静态会增加模拟的噪音。
  • 静态状态需要在使用它的每个单元测试中重置,因为不同的单元测试将更改该粘性状态。
  • 如果测试运行程序并发进行单元测试,则静态状态将无法可靠地使用该模拟程序,因为不同的线程会摆弄相同的状态,从而导致不可预测的行为。

示例2-可使用实例策略进行测试

实例策略的情况下,我们有一个取决于平台的“实际”策略:

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

我们还有一个可以在单元测试中控制的“模拟”策略:

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

使用该策略的生产类将策略类型作为模板参数,获取注入到构造器中的策略实例,并调用其实例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;
};
Run Code Online (Sandbox Code Playgroud)

在生产代码中,testable_class2使用“真实”策略实例化:

void use_testable_class2()
{
    const testable_class2<system_thread_policy2> instance;
    instance.some_function();
}
Run Code Online (Sandbox Code Playgroud)

在单元测试中,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");
}
Run Code Online (Sandbox Code Playgroud)

这种方法的优点:

  • 可以将测试交互的功能(例如上面的调用计数参数检查)添加到模拟中,并用于验证类交互单元测试。
  • 实例调用使优化程序很容易内联到的“真实”调用sleep_for
    • 模拟中没有静态状态,这使得编写,阅读和维护单元测试更加容易。

这种方法的缺点:

  • 实例状态为模拟增加了可变的噪音。
  • 实例状态会给客户端(testable_class2)增加噪音-如果不需要验证交互,则可以按构造函数中的值传递策略,并且大多数类goo都会消失。

示例3-可使用虚拟策略进行测试

这与前两个示例不同,后者依赖于虚拟调度,但是如果编译器/链接器可以检测到所操作的实例是基本类型,则可能使编译器/链接器优化虚拟调度。

首先,我们有一个生产类,它在非纯虚函数中使用“真实”策略:

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 };
};
Run Code Online (Sandbox Code Playgroud)

其次,我们有一个派生类,它在虚函数(一种模板方法设计模式)中实现“模拟”策略:

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

在生产代码中,testable_class3仅被实例化为自身:

void use_testable_class3()
{
    // Lots of opportunities to optimize away the virtual dispatch.
    testable_class3 instance;
    instance.some_function();
}
Run Code Online (Sandbox Code Playgroud)

在单元测试中,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");
}
Run Code Online (Sandbox Code Playgroud)

这种方法的优点:

  • 可以将测试交互的功能(例如上面的调用计数参数检查)添加到模拟中,并用于验证类交互单元测试。
  • 对“自身”的基类虚拟调用使优化程序可以内联到的“实际”调用sleep_for
  • 模拟中没有静态状态,这使得编写,阅读和维护单元测试更加容易。

这种方法的缺点:

  • 无法标记基类final(C ++ 11),因为必须允许从该基类继承该基类,并且如果比上述简单示例复杂得多,这可能会影响该类的其余部分设计。
  • 编译器/链接器可能不佳,或者根本无法优化虚拟调度。

测试运行

可以使用以下方法测试以上所有内容:

int _tmain(int argc, _TCHAR* argv[])
{
    test_testable_class1();
    test_testable_class2();
    test_testable_class3();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

完整的可运行示例在http://pastebin.com/0qJaQVcD