Re: Unit Testing Frameworks (was Re: Singletons)
Richard wrote:
Ian Collins <ian-news@this.is.invalid> spake the secret code
<al3diaF2gb5U1@mid.individual.net> thusly:
[...] For unit testing, you don't want to bring in all the real
dependencies of the code under test, you want to be able to monitor
and control the interfaces your unit is using. TO do that, you mock
them.
Yes, I agree completely. The difficulty of Singletons isn't that they
are a singleton per se, but that the SUT has lots of code like this:
class Single
{
public:
static Single *instance();
// other public methods but Single is not a pure virtual base
void doSomething();
void doOtherThing();
private:
Single();
~Single();
};
class SUT
{
public:
void someMethod();
// ...
};
void SUT::someMethod()
{
Single::instance()->doSomething();
// ...
Single::instance()->doOtherThing();
// ...
}
....which means I'm left only with link-time substitution as a way to
mock out the singleton if I leave this code unchanged.
Which isn't a problem if (like me) link-time substitution or
interposition is part of your standard test process.
Consider this minor refactoring, however:
class Single
{
public:
virtual ~Single() { }
// pure virtual base defining interface to Single
virtual void doSomething() = 0;
virtual void doOtherThing() = 0;
};
class ProductionSingle : public Single
{
public:
static Single *instance();
private:
ProductionSingle();
virtual ~ProductionSingle();
};
class SUT
{
public:
void someMethod() { someMethod(ProductionSingle::instance()); }
void someMethod(Single *single);
};
void SUT::someMethod(Single *single)
{
single->doSomething();
// ...
single->doOtherThing();
// ...
}
Now I can test SUT::someMethod without it being directly coupled to
the production singleton; in fact, there's NOTHING in SUT that
requires it's collaborator to be a singleton, but the choice of making
that collaborator a singleton "leaked" into the implementation of SUT
making it harder to test.
But haven't you added a new method to SUT just to avoid the coupling?
Extracting an interface over Single and
using DI (at the method level) makes testing SUT::someMethod *much*
easier and I don't have to resort to link-time substitution in order
to test the method. Existing code that calls SUT::someMethod()
doesn't have to change.
Just about everything in software development is a trade off. In your
case you have added code to SUT to facilitate testing, in mine I would
add a description of Single to the file used to generate mocks. Using
abstract classes to support testing is a perfectly valid approach, but
you hand code a test derived class where a mock framework will do the
code generation for you.
For example, given your code if you wanted to test SUT::someMethod
called doSomething(), you might have to write something like:
struct TestSingle : Single
{
bool doSomethingCalled;
bool doOtherThingCalled;
TestSingle() : doSomethingCalled(), doOtherThingCalled() {}
void doSomething() { doSomethingCalled = true; }
void doOtherThing() { doOtherThingCalled = true; }
}
void testSomeMethodCallsDoSomething()
{
TestSingle single;
SUT sut;
sut.someMethod( &single );
TEST_ASSERT( single.doSomethingCalled );
}
Which isn't too bad for one simple class, but can get messy with more
member functions and if there are parameter values to check.
With the harness I use, I would add an XML description of Single:
<Class id="Single">
<Function id="doSomething" return="void"/>
<Function id="doOtherThing" return="void"/>
<Function id="instance" return="Single*" static=true/>
</Class>
then my test would be
void testSomeMethodCallsDoSomething()
{
SUT sut;
sut.someMethod( &single );
TEST_ASSERT( Single::doSomething::Called );
}
--
Ian Collins
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]