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.
精彩评论