【问题标题】:Best simple way to mock static/global function?模拟静态/全局函数的最佳简单方法?
【发布时间】:2011-09-26 09:34:29
【问题描述】:

我有一个简单的几乎类似于价值的类,比如 Person:

class Person
{
public:
    Person(ThirdPartyClass *object);
    virtual ~Person(void);

    virtual std::string GetFullName() const;
    virtual int GetAge() const;
    virtual int GetNumberOfDaysTillBirthday() const;
};

我正在使用第三方库,ThirdPartyClass 需要调用一个名为 Destroy(第三方库的一部分)的全局/静态函数来销毁它。这个 Destroy 函数在 Person 析构函数中被调用。

现在我正在尝试对我的 Person 类进行单元测试,我需要一种方法来模拟/存根 Destroy 方法。我想我可以围绕静态 Destroy 函数编写一个包装器类,然后使用依赖注入将此包装器注入到 Person 类中,但这样做似乎只是为了在这个简单的类上调用这个函数而过分。有什么简单直接的方法可以做到这一点?或者依赖注入真的是最好的方法吗?

更新

最终我决定创建一个包含所有 3rd 方库的全局函数的类,然后使用依赖注入将这个类传递给我的 person 类的构造函数。这样我就可以去掉 Destroy 方法。虽然 person 类只使用一个函数,但库的其他函数在我的代码中的其他点被调用,并且当我需要测试这些函数时,我将面临同样的问题。

我在我的主应用程序代码中创建了这个包装类的一个实例,并在需要的地方注入它。我选择走这条路线,因为我认为它更清晰。我喜欢 Billy ONeal 的解决方案,我认为它回答了我的问题,但我意识到如果我将代码保留几个月然后回来,与依赖注入相比,我需要更长的时间才能弄清楚发生了什么。我想起了 Python 格言中的禅宗“显式胜于隐式”。而且我觉得依赖注入使正在发生的事情更加明确。

【问题讨论】:

  • 只创建一个静态/全局函数作为存根并调用它有什么问题?
  • @littleadv:嗯,我刚刚开始进行单元测试,但我的理解是你不想修改你正在测试的类来测试它。因此,如果我理解正确,通过创建一个存根 Destroy 方法并在我的 Person 类中使用它,我正在更改我的 person 类,然后我必须以某种方式在测试版本和生产版本之间切换。
  • @User - 不要修改你正在测试的类,实现你自己的ThirdPartyClass 作为存根。
  • @littleadv:我已经使用 googlemock 删除了 ThirdPartyClass,但 ThirdPartyClass 需要调用 Destroy 进行清理。
  • @littleadv:我不关注你。 Destroy 是一个全局/静态函数,它在 Person 类的析构函数中被调用(就像我写的那样)。

标签: c++ unit-testing function mocking static-functions


【解决方案1】:

简单,使用Typemock Isolator++(免责声明 - 我在那里工作)

在您的测试中添加 1 行来伪造/模拟 Destroy

全球使用:

FAKE_GLOBAL(Destroy);

公共静态使用:

WHEN_CALLED(Third_Party::Destroy()).Ignore();

私有静态使用:

PRIVATE_WHEN_CALLED(Third_Party::Destroy).Ignore();

不需要额外的代码/没有条件代码/没有不同的链接。

【讨论】:

    【解决方案2】:

    函数指针将创建一种替代另一个实现的方法。此外,它对调用者是透明的(除非他们正在做一些非常愚蠢的事情,比如调用 sizeof(funcname))。

    【讨论】:

      【解决方案3】:

      我只是在这里玩,但可能对您有用的一种方法是提供您自己的 destroy 函数并通过在 Third_Party_Lib 指针周围添加包装类来消除调用的歧义...

      #include <iostream>
      
      namespace Third_Party_Lib
      {
          struct X { };
          void destroy(X*) { std::cout << "Third_Party_Lib::destroy()\n"; }
      }
      
      template <typename T>
      struct Wrap
      {
          Wrap(const T& t) : t_(t) { }
          operator T&() { return t_; }
          operator const T&() const { return t_; }
          T t_;
      };
      
      namespace Mine
      {
      
      #if TEST_MODE
          // this destroy will be called because it's a better match
          // not needing Wrap::operator T&...
          void destroy(Wrap<Third_Party_Lib::X*>) { std::cout << "Mine::destroy()\n"; }
      #endif
      
          struct Q
          {
              Q(Third_Party_Lib::X* p) : p_(p) { }
              ~Q() { destroy(Wrap<Third_Party_Lib::X*>(p_)); }
              Third_Party_Lib::X* p_;
          };
      }
      
      #if TEST_MODE    
      int main()
      {
          Mine::Q q(new Third_Party_Lib::X);
      }
      #endif
      

      【讨论】:

        【解决方案4】:

        创建链接接缝。将你的destroy声明放在一个头文件中,然后有两个实现文件:

        // Destroy.cpp
        void Destroy()
        {
            //Code that really does destruction
        }
        

        对于测试:

        // DestroyFake.cpp
        void Destroy()
        {
            //Code that does fake destruction
        }
        

        然后将第一个文件链接到您的主二进制文件,将第二个文件链接到您的测试二进制文件。

        除此之外,你所要求的都是不可能的。链接器将对全局函数和静态方法的调用烘焙到直接跳转到被调用者代码中——没有查找或决策过程可以挂钩来创建任何类型的重载,就像你正在寻找的那样(除了 Destroy 调用某些东西这更容易被嘲笑)。

        【讨论】:

        • 您的意思是为两个版本的 Destroy 创建两个单独的静态库项目吗?这就是我知道如何链接的全部内容.. 还是您对链接文件有不同的想法?
        • @User:把通用代码放到静态库中。将第一个放在您的主 EXE 项目中,将第二个放在您的测试 EXE 项目中。无需将这两个拆分为静态库。
        • 那么Destroy头文件去哪里了?
        • @User:嗯,头文件没有编译,所以真的没关系。我自己将标头放在静态库“项目”中,但 MSVC++ 不在乎。
        猜你喜欢
        • 1970-01-01
        • 2020-03-29
        • 1970-01-01
        • 2012-03-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多