【问题标题】:Unit Testing: coding to interfaces?单元测试:接口编码?
【发布时间】:2011-09-30 22:30:23
【问题描述】:

目前我的项目由各种具体类组成。现在,当我开始进行单元测试时,看起来我应该为每个类创建一个接口(实际上使我的项目中的类数量增加了一倍)?我碰巧使用 Google Mock 作为模拟框架。见Google Mock CookBook on Interfaces。虽然之前我可能只有类Car 和Engine,但现在我将拥有抽象类(也称为C++ 接口)Car 和Engine,然后是实现类CarImplementation 和EngineImpl 或其他。这将允许我消除 Car 对 Engine 的依赖。

我在研究这个时遇到了两种思路:

  1. 仅当您可能需要多个接口时才使用接口 给定抽象的实现和/或用于公共 API, 所以不要创建不必要的接口。

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

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

【问题讨论】:

    标签: c++ unit-testing interface googlemock concrete


    【解决方案1】:

    认为您有很多选择。正如您所说,一种选择是创建接口。假设你有课

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

    要引入接口 - 您必须将 Car 更改为引擎

    class Car
    {
    public: 
        Car(Engine* engine) :
        e_(engine)
        {}
    
        void start()
        {
            // do car specific stuff
            e_->start();
    
    private:
        Engine *e_;
    };
    

    如果您只有一种类型的引擎 - 您突然使您的 Car 对象更难使用(谁创建引擎,谁拥有引擎)。汽车有很多零件 - 所以这个问题会继续增加。

    如果您想要单独的实现,另一种方法是使用模板。这消除了对接口的需求。

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

    在您的模拟中,您可以使用专门的引擎创建汽车:

    Car<MockEngine> testEngine;
    

    另一种不同的方法是向 Engine 添加方法以允许对其进行测试,例如:

    class Engine:
    {
    public:
        void start();
        bool hasStarted() const;
    };
    

    然后您可以向 Car 添加一个检查方法,或者从 Car 继承来进行测试。

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

    这需要在 Car 类中将 Engine 从私有更改为受保护。

    根据实际情况,将取决于最佳解决方案。此外,每个开发人员都有自己的圣杯,即他们认为代码应该如何进行单元测试。我的个人观点是牢记客户/客户。假设您的客户(可能是您团队中的其他开发人员)将创建汽车并且不关心引擎。因此,我不想公开引擎(我的库内部的一个类)的概念,以便我可以对它进行单元测试。我会选择不创建接口并一起测试两个类(我给出的第三个选项)。

    【讨论】:

      【解决方案2】:

      关于实现可见性的测试分为两类:黑盒测试和白盒测试

      • 黑盒测试侧重于通过其接口测试实现,并验证对其规范的调整。

      • 白盒测试测试有关不应该通常可以从外部访问的实现的详细细节。这种测试将验证实现组件是否按预期工作。因此,他们的结果对试图找出问题所在或需要维护的开发人员很感兴趣

      根据他们的定义,模拟适合模块化架构,但并不意味着项目中的所有类都需要完全模块化。当一组班级彼此了解时,画一条线是完全可以的。它们作为一个组可以从某些外观接口类的角度呈现给其他模块。但是,您仍然希望在此模块中拥有白盒测试驱动程序,并了解实现细节。因此,这种测试不适合模拟。

      由此可知,您不需要为所有内容都提供模拟或接口。只需采用实现外观接口的高级设计组件并为它们创建模拟。它将为您提供模拟测试带来回报的最佳点恕我直言

      话虽如此,请尝试根据您的需要使用该工具,而不是让该工具强迫您做出您认为从长远来看不会有益的更改

      【讨论】:

      • 感谢您的回答;我正在尝试摸索它。你是说白盒测试不适合模拟吗?我看待单元测试的方式是尝试测试每个代码单元。例如,我想测试 Car 和 Car 取决于 Engine 类。为了确保我正在进行 unit 测试而不是 integration 测试,我需要 模拟 Engine 类(& 一个以另一种方式获得Car 使用我嘲笑的Engine)。否则,我正在测试两个类并进行集成测试,而不是单元测试。为了轻松模拟Engine 类,它似乎需要一个接口。
      • 这里唯一的区别是 unit 代表什么。通常对于某些人来说,要测试的单元代表一个编译单元或一个类,但不一定如此。例如,如果您创建一个图类,节点和边将高度耦合,因此单独测试它们是毫无意义的。只需将整个图形模块包装在外观接口中,当其他模块需要与图形的模拟表示进行交互时,模拟图形外观,而不是单个类(作为适当的外观,将需要节点的 API 级别表示,例如整数或 id)
      • 在我对单元测试的理解中,如果边缘类使用节点类,那么当我在测试边缘类时,我会想模拟节点类,因为我不想测试两个“单位”同时。如果边缘类测试失败,我将不知道真正失败的是边缘还是节点。我想我会把你所说的称为集成测试(意识到它在一定程度上只是语义)。
      • 是的,老实说,我认为这是一个品味问题,你将把它想象成一个单元,以及在什么分辨率下你会停下来说“我说,这将是我的概念单元”,在我看来,边和节点是语法元素,但它们本身并没有真正的功能;它们只在相互交互中获取语义。所以对我来说,单元测试需要测试单元的 API(这里是图表)并验证它是否匹配它的预期语义(仅作为一个整体有意义)。当然你不想让这些增长太多,否则你最终会再次解开未缠结的代码
      【解决方案3】:

      为项目中的每个类创建接口可能有必要,也可能没有必要。这完全是一个设计决定。我发现它大多不是。通常在n-tier 设计中,您希望抽象数据访问和逻辑之间的层。我认为您应该朝着这个方向努力,因为它有助于测试逻辑,而无需太多测试所需的基础设施。像dependency injection and IoC 这样的抽象方法需要你做这样的事情,并且可以更容易地测试所述逻辑。

      我会检查您尝试测试的内容,并专注于您认为最容易出错的领域。这可以帮助您决定是否需要接口。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-11-27
        • 2017-12-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-03
        • 1970-01-01
        • 2020-10-30
        相关资源
        最近更新 更多