【问题标题】:What C++ design pattern is available for swapping behavior of a function template between production code and testing code?什么 C++ 设计模式可用于在生产代码和测试代码之间交换函数模板的行为?
【发布时间】:2020-10-21 14:51:03
【问题描述】:

我正在重构一个组件以使其更易于单元测试。第一步是为某些核心组件代码启用单元测试。不幸的是,这个核心组件代码调用了一些直接与其他组件的具体类交互的组件内实用函数,这些类在没有生产环境的情况下很难实例化。对于普通的实用程序函数,我可以利用依赖注入将它们包装为实用程序接口中的虚拟函数,并在单元测试期间交换覆盖。但是,也有实用函数模板,我不能应用相同的技术,因为我们不能有虚函数模板。是否有已知的模式来处理这个问题?

举一个具体的例子,考虑下面的代码。有没有办法在单元测试期间切换utilIsNameValid 函数模板的行为?有一些限制:

  1. 我需要直接将utilIsNameValid 中的逻辑与测试其使用代码隔离开来。这使我无法通过依赖注入来切换 utilGetNames 的行为,并希望它能够间接引导 utilIsNameValid 以我想要的方式。
  2. 我无法更改MyClasss 接口,因为它是具有广泛影响的遗留 API。 @Jarod42 的回答近乎完美,但需要 MyClass 成为模板,这可能会导致调用站点适应变化。

mycomponent/util/MyUtil.hpp (in-component utility code)

#include <string>
class TheirClassA;
class TheirClassB;

std::string utilGetName(TheirClassA* a); // a normal utility function

std::string utilGetName(TheirClassB* b); // a normal utility function

template<class T>
bool utilIsNameValid(T obj) {            // a utility function template
    auto name = utilGetName(obj);
    // ... perform the generic name checking ...
}

mycomponent/util/MyUtil.cpp (in-component utility code)

#include "MyUtil.hpp"
#include "theircomponent/TheirClassA.hpp"
#include "theircomponent/TheirClassB.hpp"

std::string utilGetName(TheirClassA* a) {
    return std::string{a->getNameInAsWayThatReturnsCharPtr()};
}
std::string utilGetName(TheirClassB* b) {
    return b->getNameInBsWayThatReturnsString();
}

mycomponent/core/MyClass.hpp (core component code)

class TheirClassA;
class TheirClassB;
class MyClass {
    public:
    bool canProcess(TheirClassA* a) const;
    bool canProcess(TheirClassB* b) const;
};

mycomponent/core/MyClass.cpp (core component code)

#include "MyClass.hpp"
#include "mycomponent/util/MyUtil.hpp"
bool MyClass::canProcess(TheirClassA* a) const {
    return utilIsNameValid(a) && /* some other conditions for TheirClassA objects*/;
}
bool MyClass::canProcess(TheirClassB* b) const {
    return utilIsNameValid(b) && /* some other conditions for TheirClassB objects*/;
}

production/main.cpp

#include "mycomponent/core/MyClass.hpp"
#include "theircomponent/TheirClassA.hpp"
#include "theircomponent/TheirClassB.hpp"
int main (){
    // ... tedious steps to set up TheirClassA and TheirClassB objects ...
    MyClass processor;
    if (processor.canProcess(a)) {
        // ... process a ....
    }
    if (processor.canProcess(b)) {
        // ... process b ....
    }
}

mycomponent/unittest/Test.cpp

#include "mycomponent/core/MyClass.hpp"
#include "theircomponent/TheirClassA.hpp"
#include "theircomponent/TheirClassB.hpp"
int main (){
    // ... cannot afford instantiating actual objects ...
    TheirClassA* a = nullptr;
    TheirClassB* b = nullptr;

    // ... need someway for utilIsNameValid to simply return 
    //     true or false to test out the rest of the code in
    //     MyClass::canProcess(...) ...
    MyClass processor;
    if (processor.canProcess(a)) {
        // ... process a ....
    }
    if (processor.canProcess(b)) {
        // ... process b ....
    }
}

【问题讨论】:

  • 你在使用一些单元测试框架吗?
  • 是的,这个例子只是一个简化版。在真实的代码库中,我们使用 Google Test。

标签: c++ unit-testing function-templates


【解决方案1】:

你可以制作你的类模板,比如:

// myclass.hpp
template <typename Utility>
class MyClassT {
    public:
    bool canProcess(TheirClassA* a) const;
    bool canProcess(TheirClassB* b) const;
};

class UtilityProd; // Class where you have your static methods
using MyClass = MyClassT<UtilityProd>;
// myclass.inl
template <typename Utility>
bool MyClassT::canProcess(TheirClassA* a) const
{
    return Utility::utilIsNameValid(a) && /* some other conditions for TheirClassA objects*/;
}
template <typename Utility>
bool MyClassT::canProcess(TheirClassB* b) const
{
    return Utility::utilIsNameValid(b) && /* some other conditions for TheirClassB objects*/;
}
// myclass.cpp
#include "MyClass.hpp"
#include "MyClass.inl"
#include "UtilityProd.h"


// explicit instantiation to keep separation header/cpp 
template class MyClassT<UtilityProd>;
// test.cpp
#include "MyClass.hpp"
#include "MyClass.inl"
#include "UtilityTest.h"

using MyClassTest = class MyClassT<class UtilityTest>;

int main (){
    // ... cannot afford instantiating actual objects ...
    TheirClassA* a = nullptr;
    TheirClassB* b = nullptr;


    MyClassTest processor;

    UtilityTest::setResult(..); // ...

    if (processor.canProcess(a)) {
        // ... process a ....
    }
    if (processor.canProcess(b)) {
        // ... process b ....
    }
}

production/main.cpp(和所有生产案例)保持不变。

【讨论】:

  • 这是一个有趣的想法,它确实提供了一个分离,并允许根据每个测试的需要定制 utils 的单元测试版本。不过,我对这种方法的主要问题是,我几乎不可能将生产调用站点(例如 main.cpp)更改为 MyClass,因为它是一个遗留且影响广泛的 API。让我在问题中添加这个约束。
  • @DXZ 在您得到充分回答当前状态下的问题的答案后,大幅更改问题并不好。
  • @DXZ:我提供了方法(using)以避免更改呼叫站点(您的产品 Main 应该保持不变)。
  • 我很抱歉,但这种限制确实是我的意思,但在我原来的帖子中没有很好地沟通。我确实承认@Jarod42,如果有办法让我相信他/她,我很乐意这样做,但不是在通过讨论澄清/最终确定问题之前将其标记为最佳答案。否则,StackOverlow 的“编辑”功能是干什么用的?
  • 您应该避免以使现有答案无效的方式编辑问题。
猜你喜欢
  • 1970-01-01
  • 2012-03-28
  • 1970-01-01
  • 2020-03-17
  • 1970-01-01
  • 2012-08-20
  • 2023-03-26
  • 2010-11-20
  • 1970-01-01
相关资源
最近更新 更多