开发者

Is there a way to break this dependency for unit testing?

开发者 https://www.devze.com 2023-02-15 13:05 出处:网络
My class A is dependent to class B. Here is the code //declaration class A { public: A(B *b); ~A(); void m1();

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 depend开发者_如何学运维ency(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?


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);


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消