单元测试:是否应该编写接口代码?

15

目前我的项目由各种具体类组成。现在,当我开始进行单元测试时,似乎我应该为每个类创建一个接口(实际上会使我的项目中的类数量翻倍)?我恰好使用的是 Google Mock 作为模拟框架。请参见Google Mock CookBook on Interfaces。以前,我可能只有CarEngine这样的类,现在我将拥有抽象类(也就是 C++ 接口)CarEngine,然后是实现类CarImplementationEngineImpl或其他名称的类。这将允许我替换掉CarEngine的依赖。

我在研究中发现了两种思路:

  1. 仅在您可能需要给定抽象的多个实现和/或在公共 API 中使用时才使用接口,因此不必无谓地创建接口。

  2. 单元测试存根/模拟通常是“其他实现”,因此,是的,您应该创建接口。

在进行单元测试时,我应该为项目中的每个类创建一个接口吗?(我倾向于为了方便测试而创建接口)

3个回答

8

您有很多选择。正如您所说,其中一个选项是创建接口。假设您有类

class Engine:
{
public:
    void start(){ };
};

class Car
{
public: 
    void start()
    {
        // do car specific stuff
        e_.start();

private:
    Engine e;
};

介绍接口 - 你需要将Car更改为接受Engine

class Car
{
public: 
    Car(Engine* engine) :
    e_(engine)
    {}

    void start()
    {
        // do car specific stuff
        e_->start();

private:
    Engine *e_;
};

如果你只有一种类型的引擎,那么你的汽车对象将变得更难使用(谁创建引擎,谁拥有引擎)。汽车有很多部件 - 所以这个问题将会不断增加。

如果你想要不同的实现,还可以使用模板。这样就不需要接口了。

class Car<type EngineType = Engine>
{
public: 
    void start()
    {
        // do car specific stuff
        e_.start();

private:
    EngineType e;
};

在你的模拟中,你可以使用专门的引擎创建汽车:
Car<MockEngine> testEngine;

另一种不同的方法是向引擎添加方法以进行测试,类似于以下内容:
class Engine:
{
public:
    void start();
    bool hasStarted() const;
};

你可以在Car类中添加一个检查方法,或者从Car继承来测试。

class TestCar : public Car
{
public:
    bool hasEngineStarted() { return e_.hasStarted(); }
};

这将需要将Car类中的Engine从private更改为protected。

根据实际情况,最佳解决方案会有所不同。此外,每个开发人员都有自己认为代码应该如何进行单元测试的方法。我的个人观点是要考虑客户/用户。假设您的客户(可能是团队中的其他开发人员)将创建汽车而不关心引擎。因此,我不希望暴露引擎的概念(一个库内部的类),只是为了进行单元测试。我选择不创建接口并将两个类一起测试(我提供的第三个选项)。


2

关于实现可见性,测试有两个类别:黑盒测试和白盒测试。

  • 黑盒测试侧重于通过它们的接口测试实现,并验证其符合规范。

  • 白盒测试测试实现的细节,这些细节通常不应从外部访问。这种测试将验证实现组件按预期工作。因此,它们的结果主要对试图找出故障或需要维护的开发人员感兴趣。

模拟根据其定义适用于模块化架构,但并不意味着项目中的所有类都需要完全模块化。当一组类将彼此了解时,画一些线是完全可以的。它们作为一组可以从某个门面接口类的角度向其他模块展示。然而,在此模块内仍需要具有有关实现细节的白盒测试驱动程序的知识。因此,这种测试不适合模拟。

由此显然得出结论,您不需要为所有内容都创建模拟或接口。只需获取实现门面接口的高级设计组件并为其创建模拟即可。这将为您提供模拟测试的最佳效果。

话虽如此,尝试根据您的需求使用工具,而不是让工具强制您进行您认为从长远来看不会有益的更改。


谢谢你的回答;我正在努力理解它。你是说白盒测试不适合使用模拟吗?我对单元测试的看法是,我试图测试每个代码单元。例如,我想测试Car,而Car依赖于一个Engine类。为了确保我进行的是单元测试而不是集成测试,我需要模拟Engine类(并以某种方式让Car使用我的模拟Engine)。否则,我就在测试两个类并进行集成测试,而不是单元测试。为了轻松地模拟Engine类,它似乎需要是一个接口。 - User
这里唯一的区别在于单元代表什么。通常,对于某些人来说,要测试的单元代表编译单元或类,但不一定是这样。例如,如果您创建了一个图形类,则节点和边将高度耦合在一起,因此在隔离中测试它们是毫无意义的。只需将整个图形模块包装在外观接口中,当其他模块需要与图形的模拟表示交互时,模拟图形外观,而不是单独的类(作为适当的外观,将需要节点的API级别表示,如整数或ID)。 - lurscher
在我对单元测试的理解中,如果边缘类使用节点类,那么当我测试边缘类时,我会想要模拟节点类,因为我不想同时测试两个“单元”。如果边缘类测试失败,我将不知道是边缘还是节点真正失败了。我认为你所说的是集成测试(意识到这只是在一定程度上的语义学问题)。 - User
是的,说实话,我认为这取决于个人喜好,你会将什么视为一个单元,并在何时停止并说“这就是我的概念单元”,在我看来,边缘和节点是语法元素,但本身没有真正的功能;它们只能通过相互作用获得其语义。因此,对我来说,单元测试需要测试单元的API(在这里是图形),并验证它是否符合预期的语义(仅作为整体有意义)。当然,您不希望将其扩展太多,否则您最终会再次解开纷乱的代码。 - lurscher

0

为项目中的每个类创建接口可能是必要的,也可能不是。这完全取决于设计决策。我发现大多数情况下并不需要。在n-tier设计中,通常希望抽象出数据访问和逻辑之间的层。我认为你应该朝着这个方向努力,因为它有助于测试逻辑,而不需要太多基础设施来进行测试。像依赖注入和IoC这样的抽象方法需要你做类似的事情,并且会使测试逻辑变得更容易。

我建议你检查一下你想要测试的内容,并关注那些你认为最容易出错的领域。这可以帮助你决定是否需要接口。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接