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

My class A depends on 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 create an instance of class A and call its m1 () method with the following code

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

The m1 () method calls the B m2 () method because B m2 () is not virtual. class B is legacy code from another module. I do not want to change his code. but also I do not want to change the class A code.

Any solution to break this addiction?

+4
source share
3 answers

Firstly, this is a bad construct with delete ptr_b; in the class A destructor, since the constructor of A. does not have new B() . This means that each time an instance of A is created, you transfer ownership of B to A, leaving you with the potential risk of duplicate delete for those who use A, who do not know the internal components.

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

 class FakeB : public InterfaceB 

and

 class B : public InterfaceB 

therefore, all member functions from A can use parameters of type InterfaceB * instead of B * . Then injecting the FakeB object into A becomes clearly easy.

Unfortunately, this means you need to change B (at least a little). If this is not an option, it is always possible to wrap B with some WrapperB class (this is basically the same idea as in the classic Adapter Template ):

 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, simple code for delegating methods, for which you can omit unit tests. And you should use WrapperB instead of B when you are going to use it in combination with A. But what you get is ideal for testing class A

Another (possibly even better) option creates the WrapperB class in such a way that you enter a reference to object B 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 like this:

 B b; A a(WrapperB(b)); FakeB fb; A a_for_test(fb); 
+6
source

Merhaba Onur

Another idea is to use some preprocessor characters to switch class A code between normal and unit testing. For instance:

A.hpp File

 #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 will be a preprocessor symbol that you would only enable when creating the unit test.

If the Testable_B.hpp file contains a class with a name other than "B" (for example, Testable_B), you will also need to add these directives to the definition of class A. The disadvantage is that if more such changes are required, this would create a mess in the definition class.

Another way is 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 that this is not a very elegant solution, but you may find it useful if you do not want to change the class A code. If you absolutely do not want to make any changes to the source code, then probably stefaanv solution is the way to go .

+2
source

The ability to break the dependency is to change the path to include in your makefile and include your version of class B. I cannot say if this works in your device testing scheme.

+1
source

Source: https://habr.com/ru/post/1342950/


All Articles