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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With