有没有办法打破单元测试的这种依赖性?

发布于 2024-10-20 17:54:32 字数 777 浏览 3 评论 0原文

我的 A 类依赖于 B 类。 的代码

//declaration
class A
{
  public:
    A(B *b);
    ~A();
    void m1();
  private:
    B *ptr_b;
};

//implementation
A::A(B *b)
{
  ptr_b = b;
}

A::~A()
{
   delete ptr_b;
}

void A::m1()
{
   ptr_b->m2();
}

这是我想通过以下解决方案打破这种依赖性(用于单元测试) 。 这是代码

 class FakeB : public B 
  {    
     public:
       FakeB();
       ~FakeB();
       virtual void m2() = 0; 
  };

 class StubB : public FakeB 
 {   
   public:
      StubB();
      ~StubB();
      void m2(); 
 }

但是当我实例化类 A 并使用以下代码调用其方法 m1() 时,

A *ptr_a = new A(new StubB);
ptr_a->m1();

方法 m1() 调用 B 的方法 m2() 因为 B 的 m2() 不是虚拟的。 B 类是来自另一个模块的遗留代码我不想更改它的代码 但我也不想更改 A 类的代码。

有什么解决方案可以打破这种依赖性吗?

My class A is dependent to class B.
Here is the code

//declaration
class A
{
  public:
    A(B *b);
    ~A();
    void m1();
  private:
    B *ptr_b;
};

//implementation
A::A(B *b)
{
  ptr_b = b;
}

A::~A()
{
   delete ptr_b;
}

void A::m1()
{
   ptr_b->m2();
}

I want to break this dependency(for unit testing) with the following solution.
Here is the code

 class FakeB : public B 
  {    
     public:
       FakeB();
       ~FakeB();
       virtual void m2() = 0; 
  };

 class StubB : public FakeB 
 {   
   public:
      StubB();
      ~StubB();
      void m2(); 
 }

But when I instantiate class A and call its method m1() with the following code

A *ptr_a = new A(new StubB);
ptr_a->m1();

Method m1() calls B's method m2() because B's m2() is not virtual.
class B is legacy code from another module I do not want to change its code
but also I do not want to change class A's code.

Any solution to break this dependency?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

攀登最高峰 2024-10-27 17:54:32

首先,在类 A 的析构函数中使用 delete ptr_b; 是一个糟糕的设计,因为 A 的构造函数中没有 new B()。这意味着每次创建 A 的实例后,您会将 B 对象的所有权转移给 A,这会给您带来潜在的风险,即使用 A 且不了解其内部结构的人会出现重复删除的情况。

其次,如果您想给 A 一个“存根”(或“模拟”或“假”)对象而不是“真实的 B”,则需要 BFakeB一个公共接口,包含 B 中 A 需要的所有方法作为虚拟方法:

class FakeB : public InterfaceB 

因此

class B : public InterfaceB 

A 的所有成员函数都可以使用 InterfaceB * 类型的参数,而不是 B *。然后将 FakeB 对象注入 A 显然很容易。

不幸的是,这意味着你必须改变 B (至少,一点点)。如果这不是一个选项,那么总是有可能通过某个类 WrapperB 来包装 B
(它与经典适配器模式中的想法基本相同):

class WrapperB: public InterfaceB 
{
    B _b;
 public:
    WrapperB(/* some parameters */) : _b(/* same parameters */){}

    // Here you need to implement all methods of
    // InterfaceB and delegate them to the original method calls
    // of _b. You should give them the same name and signature as
    // the corresponding (non-virtual) methods in B.
    // For example, if there is a method m2 in B, 
    // there should be a pure virtual method m2 in InterfaceB, and
    // an implementation here like this:
    virtual void m2(){ _b.m2(); }
};

WrapperB 将仅包含非常简单、直接的方法委托代码,您可以省略单元测试。当您要将它与 A 结合使用时,您必须使用 WrapperB 而不是 B。但是您得到的是一个完全可单元测试的 class A< /代码>。

另一个(也许更好)变体是以从外部注入对 B 对象的引用的方式构造 WrapperB 类:

class WrapperB: public InterfaceB 
{
    B& _b;
 public:
    WrapperB(B& b) :_b(b){}

    // implement InterfaceB methods as above
    virtual void m2(){ _b.m2(); }

}

您可以像这样使用它:

B b;
A a(WrapperB(b));

FakeB fb;
A a_for_test(fb);

First, it is bad design having a delete ptr_b; in the destructor of class A since there is no new B() in the constructor of A. That means every time an instance of A is created, you are transfering ownership of the B object to A, leaving you with the potential risk of a duplicate delete for someone using A who does not know the internals.

Second, if you want to give A a "stub" (or "mock", or "fake") object instead of a "real B", B and FakeB need a common interface containing all methods from B which A needs as virtual methods:

class FakeB : public InterfaceB 

and

class B : public InterfaceB 

so all member functions of A can use parameters of type InterfaceB * instead of B *. Then injecting a FakeB object into A gets obviously easy.

Unfortunately, that would mean you have to change B (at least, a little bit). If that is not an option, there is always the possibility of wrapping B by some class WrapperB
(it is mostly the same idea as in the classic Adapter pattern):

class WrapperB: public InterfaceB 
{
    B _b;
 public:
    WrapperB(/* some parameters */) : _b(/* same parameters */){}

    // Here you need to implement all methods of
    // InterfaceB and delegate them to the original method calls
    // of _b. You should give them the same name and signature as
    // the corresponding (non-virtual) methods in B.
    // For example, if there is a method m2 in B, 
    // there should be a pure virtual method m2 in InterfaceB, and
    // an implementation here like this:
    virtual void m2(){ _b.m2(); }
};

WrapperB will contain only very simple, straightforward method delegation code for which you can omit unit tests. And you have to use WrapperB instead of B when you are going to use it in conjunction with A. But what you get is a perfectly unit testable class A.

Another (perhaps even better) variant is constructing the WrapperB class in a manner where you inject a reference to a B object from outside into it:

class WrapperB: public InterfaceB 
{
    B& _b;
 public:
    WrapperB(B& b) :_b(b){}

    // implement InterfaceB methods as above
    virtual void m2(){ _b.m2(); }

}

You can use it just like this:

B b;
A a(WrapperB(b));

FakeB fb;
A a_for_test(fb);
前事休说 2024-10-27 17:54:32

Merhaba Onur

另一个想法是使用一些预处理器符号在正常模式和单元测试模式之间切换 A 类代码。例如:

文件 A.hpp

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
#endif

UNIT_TESTING 将是一个预处理器符号,仅在构建单元测试时才启用它。

如果文件 Testable_B.hpp 包含名称不是“B”的类(例如 Testable_B),您还需要在类 A 的定义中添加这些指令。缺点是,如果需要更多此类修改,这将导致使类定义变得混乱。

另一种方法是使用 typedef:

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
  typedef Testable_B B;
#endif

我知道这不是非常优雅的解决方案,但如果您不想修改 A 类代码,也许您会发现它很有用。如果您绝对不想对源代码进行任何更改,那么 stefaanv 的解决方案可能是最佳选择。

Merhaba Onur

Another idea would be to use some preprocessor symbols to switch class A code between normal and unit-testing mode. For example:

File A.hpp

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
#endif

UNIT_TESTING would be a preprocessor symbol which you would enable only when building the unit test.

In case if file Testable_B.hpp contains class with another name than "B" (for example, Testable_B) you would also need to add these directives in the definition of class A. The drawback is that if more such modifications were needed, this would make a mess in class definition.

Yet another way would be to use typedef:

#ifndef UNIT_TESTING
# include "B.hpp" // contains "normal" class B
#else
# include "Testable_B.hpp" // contains "fake" class B, dedicated for unit testing.
  typedef Testable_B B;
#endif

I know it is not very elegant solution, but maybe you will find it useful if you don't want to modify class A code. In case you absolutely don't want to make any changes to the source code, then probably stefaanv's solution is the way to go.

oО清风挽发oО 2024-10-27 17:54:32

打破依赖性的一种可能性是更改 makefile 中的包含路径并包含您的 B 类版本。我无法判断这是否适用于您的单元测试方案。

A possibility to break the dependency is to change the include path in your makefile and to include your version of class B. I can't tell if this works in your unit testing scheme.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文