8

我在一个遗留的 C++ 代码库中工作,我想在一个类上测试一些方法,该类DependsOnUgly具有一个在文件系统上有很多外部依赖项的大类 ( Ugly) 上不容易破坏的依赖项,等等。我想得到至少有一些方法DependsOnUgly在测试中,同时尽可能少地修改现有代码。如果不进行大量代码修改,就无法通过工厂方法、方法参数或构造函数参数创建接缝;Ugly是一个直接依赖的具体类,没有任何抽象基类,并且有很多方法,很少或没有标记virtual,完全模拟它会非常乏味。我没有可用的模拟框架,但我想得到DependsOnUgly正在测试中,因此我可以进行更改。如何打破对Ugly方法进行单元测试的外部依赖关系DependsOnUgly

4

1 回答 1

10

使用我称之为预处理器模拟的东西——通过预处理器接缝注入的模拟。

我首先在 Programmers.SE 上的这个问题中发布了这个概念,通过答案我判断这不是一个众所周知的模式,所以我认为我应该分享它。我很难相信以前没有人做过这样的事情,但是因为我找不到它的文档,所以我想我会与社区分享。

为了举例,Ugly这里是概念上的实现。NotAsUgly

DependsOnUgly.hpp

#ifndef _DEPENDS_ON_UGLY_HPP_
#define _DEPENDS_ON_UGLY_HPP_
#include <string>
#include "Ugly.hpp"
class DependsOnUgly {
public:
    std::string getDescription() {
        return "Depends on " + Ugly().getName();
    }
};
#endif

丑陋的.hpp

#ifndef _UGLY_HPP_
#define _UGLY_HPP_
struct Ugly {
    double a, b, ..., z;
    void extraneousFunction { ... }
    std::string getName() { return "Ugly"; }
};
#endif

有两种基本的变体。第一个是只有某些方法Ugly被 调用DependsOnUgly,并且您已经想模拟这些方法。第二个是

技术1:替换所有Ugly使用的行为DependsOnUgly

我将这种技术称为预处理器部分模拟,因为模拟只实现了被模拟类的接口的必要部分。在模拟类的头文件中使用与生产类同名的包含保护,以导致生产类永远不会被定义,而是模拟。一定要包括前面的模拟DependsOnUgly.hpp

(请注意,我的测试文件示例不是自我验证的;这只是为了简单起见,并且与单元测试框架无关。重点是文件顶部的指令,而不是实际的测试方法本身.)

测试.cpp

#include <iostream>
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; } // All that DependsOnUgly depends on
};
#endif

技术2:替换一些Ugly使用的行为DependsOnUgly

我称其为Subclassed-in-Place Mock,因为在这种情况下,Ugly它是子类化的,并且必要的方法被覆盖,而其他方法仍然可以使用——但子类的名称仍然是Ugly. 定义指令用于重命名UglyBaseUgly; 然后使用 undefine 指令和模拟Ugly子类BaseUgly。请注意,这可能需要Ugly根据具体情况将某些内容标记为虚拟。

测试.cpp

#include <iostream>
#define Ugly BaseUgly
#include "Ugly.hpp"
#undef Ugly
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly: public BaseUgly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; }
};
#endif

请注意,这两种方法都有点不稳定,应谨慎使用。随着更多的代码库正在测试中,它们应该被移走,并尽可能用更标准的方法来打破依赖关系。请注意,如果遗留代码库的包含指令足够混乱,它们都可能会变得无效。但是,我已经成功地将它们用于实际的遗留系统,所以我知道它们可以工作。

于 2013-04-15T19:44:11.247 回答