【问题标题】:C++ High performance unit testing with Google Mock?使用 Google Mock 进行 C++ 高性能单元测试?
【发布时间】:2014-02-20 20:44:42
【问题描述】:

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

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

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

【问题讨论】:

  • 投反对票?为什么?这是一个有效的问题。我正在寻找一个更好的解决方案,如果我知道一个我永远不会发布的解决方案。当然,如果这个问题被正确回答,它将帮助其他几个有类似情况的人。
  • 我认为投反对票是由于您措辞的(有趣但)固执己见。如果您讨厌单例或这种间接级别会给您带来什么症状,这与问题无关。我试图让你的问题更加中立。
  • @fish 非常感谢。
  • 将我的 3 个示例作为可运行代码添加到 pastebin.com/0qJaQVcD
  • 查看下面的示例 2 - 可使用实例策略进行测试以获得答案。

标签: c++ unit-testing interface googlemock static-classes


【解决方案1】:

首先,虚拟调度并不那么昂贵,因此您可能正在做微优化。即使是嵌入式平台。

如果你真的想避免虚拟调度,你可以使用静态多态,模板,并通过模板参数注入系统调用。像这样:

struct SysCallCaller
{
    static void doCall()
    {
        // really execute system call
    }
    private:
    SysCallCaller();
};

struct SysCallMock
{
    static void doCall()
    {
        // mock system call
    }
};

template < typename SysCallType >
struct MyClass
{
    void Foo()
    {
        SysCallType::doCall();
    }

};

int main()
{
#ifdef CALL_MOCK
    MyClass< SysCallMock > obj;
#else
    MyClass< SysCallCaller > obj;
#endif

    obj.Foo();
}

最重要的是,尽量避免单例。它们只是隐藏的全局变量。

【讨论】:

  • 您的示例使用了两个额外的类——这正是我不想做的。我不反对使用 v-table,我反对浪费内存。我正在处理 RAM 以 KB 为单位的系统。
  • @Zak 好的,现在只有一个,而且此类调用很可能已被优化掉。
  • 您正在制作静态类...这距离单例仅一步之遥,只是您没有阻塞构造函数。为什么你的解决方案更好?
  • @Zak C++ 中没有静态类这样的东西。模板类中的方法需要静态方法。您不想创建对象来节省一些内存。
  • 没错,但是struct == class。因此,您实际上是在制作静态类(或单例)——无论您想如何称呼它们。
【解决方案2】:

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

如果您在嵌入式环境中虚拟调度开销太大,或者该架构的编译器/链接器优化器做得非常糟糕,那么您可以尝试以下 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

【讨论】:

  • 好帖子,谢谢!关于我们模拟 sleep_for 的这个特定示例的注意事项 - 当模拟 sleep 时,我会说任何潜在的调用开销都完全无关紧要,因为无论如何我们都会休眠几毫秒 :-)
  • 感谢您的深入回复! Example 2 - Testable Using Instance Policy 是 Google Mock 范围内的最佳答案。另外,在示例 #3 中,基类上的虚拟方法是否必须标记为受保护而不是私有才能传递给子类?
  • @Zak: "在示例 #3 中,基类上的虚拟方法是否必须标记为受保护而不是私有才能传递给子类?" -不会。通过在基类中将其设为私有,可以防止派生类调用它的基类版本,因为只有基类可以访问它。另外,正如我所说,"和完整的可运行示例位于pastebin.com/0qJaQVcD" - 它真的 构建并运行:)
  • testable_class3 有一个私有虚拟方法 - 为什么? mock_testable_class3 没有继承这个方法,对吗?您能否更新您的帖子以解释为什么这些私有方法需要是虚拟的或有什么用途?
  • @Zak:如果您没有在构造函数中提供策略,它将默认为“真实”策略 (testable_class2(const thread_policy&amp; policy = thread_policy()) : m_thread_policy(policy) { }),并且因为您将该 RValue 绑定到 const 引用,它由标准保证工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多