Hiding global methods, a C++ example 28
Background
This is a continuation of this discussion. A few of the postings by GBGames have been swallowed so the context is somewhat lost. What follows is an excerpt from an email he sent me and some example C++ code that demonstrates making global methods “testable” in a sense. Or rather, making your code not use global methods during test.The Offending Method
Here is a method that GBGames has under test:void SDLHardwareLayer::initializeHardware()
{
const SDL_version * sdlVersion = SDL_Linked_Version();
if (sdlVersion->minor < 2 && sdlVersion->major < 2)
{
//Error message
}
else if (SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO |
SDL_INIT_NOPARACHUTE) < 0)
{
//Error message
}
else
{
m_screen = SDL_SetVideoMode(m_x, m_y, m_bitDepth, SDL_DOUBLEBUF);
if (NULL != m_screen)
{
SDL_WM_SetCaption(m_title.c_str(), 0);
}
}
}
This is how this code is organized:
So What’s Wrong?
One problem he notes is that the test code driving this production code causes windows to pop up during testing. Is this a problem? If it works, then it may be OK. However, you can remove that and also make test-ability a bit easier. You’ll be testing interactions and responses rather than directly using the library through its global functions.Global Functions Considered Harmful
First off, this code as written directly uses global functions. If you leave this code unchanged, then the only option you have to “fix” the problem of windows popping up is a link-seam (see Working Effective with Legacy Code, page 233-234. In a nutshell, a link seam will link different versions of a library, one for testing, one for actual execution. So you’ll need some “make fu” to get the link seam working in a manner that easily allows building for testing versus building for execution. Since we’re using C++ and we own this code, I chose a different route:- Change this code to use an object that uses the library
- That object implements an interface (a base class with all pure virtual methods)
- Change this code so that it acquires this object through a factory (I could have simply used constructor injection, but I’ve already written it using a configurable factory [really singleton] – but it would have been simpler if I had injected the interface)
- Created a test double that can be used when writing new test doubles making code maintenance a bit easier (mentioned in the other blog posting).
- Write test-method specific test doubles
Changing Code to Use Factory + Object w/virtual Methods
Rather than directly calling the various global SDL_* methods, e.g.:const SDL_version * sdlVersion = SDL_Linked_Version();
const SDL_version * sdlVersion = SdlLayerFactory::getInstance()->SDL_Linked_Version();
Base Interface
To make this work, I created a base interface representing all of the methods needed by the SDLHardwareLayer::initializeHardware:#pragma once
struct SDL_version;
class SdlLayerInstanceDelegator {
public:
virtual ~SdlLayerInstanceDelegator(void) = 0;
virtual const SDL_version *SDL_Linked_Version() = 0;
virtual int SDL_Init(int bitMask) = 0;
virtual void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode) = 0;
virtual void SDL_WM_SetCaption(const char *title, int someIntValue) = 0;
protected:
SdlLayerInstanceDelegator();
private:
SdlLayerInstanceDelegator(const SdlLayerInstanceDelegator &rhs);
SdlLayerInstanceDelegator &operator = (const SdlLayerInstanceDelegator &);
};
Real Calls Global Methods
Getting rid of the direct call to the global method makes this work. However, you still need to call those global methods somewhere. That’s where the “real” version of the interface comes in. The real version of this class simply calls the global methods (code only, not showing header file right now):const SDL_version *RealSdlLayerInstanceDelegator::SDL_Linked_Version() {
return ::SDL_Linked_Version();
}
int RealSdlLayerInstanceDelegator::SDL_Init(int bitMask) {
return ::SDL_Init(bitMask);
}
void *RealSdlLayerInstanceDelegator::SDL_SetVideoMode(int x, int y, int bitDepth, int mode) {
return ::SDL_SetVideoMode(x, y, bitDepth, mode);
}
void RealSdlLayerInstanceDelegator::SDL_WM_SetCaption(const char *title, int someIntValue) {
::SDL_WM_SetCaption(title, someIntValue);
}
The Factory: Inversion of Control
The original code directly used global methods. We need to invert this relationship so that rather than depending directly on those methods, it depends on an interface. Then if it happens that it uses a “real” version, the underlying code will call the actual library. If, however, it is instead using a test double, it will not. It will do whatever the test double dictates.We cannot leave the decision of which object to talk to in the SdlHardwareLayer class. Instead, I’ve used a configurable factory (singleton). A test can configure the factory, call the method under test, and then reset the factory back after the test.
The SdlLayerFactory allows such use(again, code only, no header file):SdlLayerInstanceDelegator *SdlLayerFactory::instance = 0;
SdlLayerInstanceDelegator *SdlLayerFactory::getInstance() {
return instance;
}
SdlLayerInstanceDelegator *SdlLayerFactory::replaceInstance(SdlLayerInstanceDelegator *replacement) {
SdlLayerInstanceDelegator *original = instance;
instance = replacement;
return original;
}
Handling Growing Interfaces
One problem with “interfaces” in any language is that as you add methods to them, their implementations need to be updated. Since the SdlLayerInstanceDelegator class is not complete yet, this will case problems with test doubles. There are three ways to handle this problem: * Deal with it, as you add new methods to the interface, update all existing classes * Use a mocking library – I have not used any for C++, but if this were Java I’d use Mockito and if this were .Net I’d use Moq. * Create a base test double that implements all of the methods. All test doubles derive from that and only implement the methods needed for test.The last option does not fix the problem, it controls it. When new methods are added, you update one test double class and all other classes still work.
This may not sound like much of an issue, but in general you’ll have many small test doubles rather than a few large test doubles (or at least that’s the standard recommendation). Why? Because a small, focused test double is less likely to be broken. It also better expressed your intention.
Here is that test double (header file excluded again): TestDoubleSdlLayerInstanceDelegator.h
#pragma once
#include "SdlLayerInstanceDelegator.h"
class TestDoubleSdlLayerInstanceDelegator: public SdlLayerInstanceDelegator {
public:
TestDoubleSdlLayerInstanceDelegator();
virtual ~TestDoubleSdlLayerInstanceDelegator();
const SDL_version *SDL_Linked_Version();
int SDL_Init(int bitMask);
void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode);
void SDL_WM_SetCaption(const char *title, int someIntValue);
};
TestDoubleSdlLayerInstanceDelegator.cpp
#include "TestDoubleSdlLayerInstanceDelegator.h"
TestDoubleSdlLayerInstanceDelegator::TestDoubleSdlLayerInstanceDelegator(){}
TestDoubleSdlLayerInstanceDelegator::~TestDoubleSdlLayerInstanceDelegator(){}
const SDL_version *TestDoubleSdlLayerInstanceDelegator::SDL_Linked_Version() {
return 0;
}
int TestDoubleSdlLayerInstanceDelegator::SDL_Init(int bitMask) {
return 0;
}
void *TestDoubleSdlLayerInstanceDelegator::SDL_SetVideoMode(int x, int y, int bitDepth, int mode) {
return 0;
}
void TestDoubleSdlLayerInstanceDelegator::SDL_WM_SetCaption(const char *title, int someIntValue){}
A Test
Finally, we need some tests and some test-specific test-doubles:#include <CppUTest/TestHarness.h>
#include "sdl.h"
#include "SdlLayerFactory.h"
#include "SdlHardwareLayer.h"
#include "TestDoubleSdlLayerInstanceDelegator.h"
TEST_GROUP(SdlHardwareLayerTest) {
virtual void setup() {
original = SdlLayerFactory::getInstance();
layer = new SdlHardwareLayer;
}
virtual void teardown() {
if(SdlLayerFactory::getInstance() != original)
delete SdlLayerFactory::getInstance();
SdlLayerFactory::replaceInstance(original);
delete layer;
}
SdlLayerInstanceDelegator *original;
SdlHardwareLayer *layer;
};
struct MajorMinorSdlhardwareLayer : public TestDoubleSdlLayerInstanceDelegator {
struct SDL_version v;
MajorMinorSdlhardwareLayer() {
v.major = 0;
v.minor = 0;
}
const SDL_version *SDL_Linked_Version() {
return &v;
}
};
TEST(SdlHardwareLayerTest, ItGeneratsErrorWithLowMajorMinorVersionNumber) {
SdlLayerFactory::replaceInstance(new MajorMinorSdlhardwareLayer);
try {
layer->initializeHardware();
FAIL("Should have thrown int value");
} catch (int value) {
LONGS_EQUAL(1, value);
}
}
struct InitFailingSdlHardwareLayer : public MajorMinorSdlhardwareLayer {
InitFailingSdlHardwareLayer () {
v.major = 3;
v.minor = 3;
}
int SDL_Init(int bitMask) {
return -1;
}
};
TEST(SdlHardwareLayerTest, ItGeneratesErrorWhenInitReturnsLessThan0) {
SdlLayerFactory::replaceInstance(new InitFailingSdlHardwareLayer);
try {
layer->initializeHardware();
FAIL("Should have thrown int value");
} catch (int value) {
LONGS_EQUAL(2, value);
}
}
Notice that there are two test doubles. Also notice that I create them as full (implicitly) inlined classes. Virtual methods and inline methods do not interact well. C++ will make a copy of the method bodies in every .o and increase link times. However, these test classes are only used in one place, so this really does not cause any problems.
In lieu of a mocking library, this is how I’d really write this code.The Final Big Picture
Here’s an image of the completed product:Summary
So is this overkill? Is using the actual SDL library causing problems? Is it OK for windows to appear during test? I’m neutral on that subject. The question I want to know is: are the tests working?- Do they add value or are they busy work.
- Do they allow me to make small, incremental steps towards a complete, working implementation
- Do they run fast enough?
- Will they run in any environment? (Can I run on a headless system?)
- Can I run multiple tests at the same time? (Not generally an issue, but it can be.)
If the SDL library is not causing a problem, then this might be overkill. However, on a long-lived project, this little amount of extra work will pay big dividends.
Oh, how long did this really take? Around 1.5 hours. But that included:- Starting a Windows XP VM
- Creating a new projec
- Setting up the link paths and include paths
- Creating the initial file and the necessary support to get it to compile
- Writing the additional classes
In fact, the actual work was probably < 30 minutes total. So while this might look big, in fact it is not. It’s nearly an idiom, so it’s mostly a matter of putting the hooks in place. So is full isolation from SDL worth 30 minutes?
Yes!
All your source files are belong to us
If this looks like a complete example, it is. I have this building and running in Visual Studio 2008. I took GBGame’s original method and got it to compile and link with a minimal set of additional source files. Then I made the structural changes to support test. And here’s all of the source code, file by file:sdl.h
#pragma once
extern "C" {
const int SDL_INIT_VIDEO = 1;
const int SDL_INIT_AUDIO = 2;
const int SDL_INIT_NOPARACHUTE = 4;
const int SDL_DOUBLEBUF = 8;
struct SDL_version {
int major;
int minor;
};
const SDL_version *SDL_Linked_Version();
int SDL_Init(int bitMask);
void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode);
void SDL_WM_SetCaption(const char *title, int someIntValue);
};
sdl_implementation.cpp
#include "sdl.h"
const SDL_version *SDL_Linked_Version() {
return 0;
}
int SDL_Init(int bitMask) {
return - 1;
}
void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode) {
return 0;
}
void SDL_WM_SetCaption(const char *title, int someIntValue){}
SdlHardwareLayer.h
#pragma once
#include <string>
class SdlHardwareLayer {
public:
SdlHardwareLayer();
virtual ~SdlHardwareLayer();
void initializeHardware();
private:
int m_x;
int m_y;
int m_bitDepth;
std::string m_title;
void *m_screen;
};
SdlHardwarelayer.cpp
#include "SdlHardwareLayer.h"
#include "sdl.h"
#include "SdlLayerFactory.h"
#include "SdlLayerInstanceDelegator.h"
SdlHardwareLayer::SdlHardwareLayer() {
}
SdlHardwareLayer::~SdlHardwareLayer() {
}
void SdlHardwareLayer::initializeHardware() {
const SDL_version * sdlVersion = SdlLayerFactory::getInstance()->SDL_Linked_Version();
if (sdlVersion->minor < 2 && sdlVersion->major < 2) {
throw 1;
}
else if (SdlLayerFactory::getInstance()->SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO |
SDL_INIT_NOPARACHUTE) < 0) {
throw 2;
}
else {
m_screen = SDL_SetVideoMode(m_x, m_y, m_bitDepth, SDL_DOUBLEBUF);
if (0 != m_screen) {
SDL_WM_SetCaption(m_title.c_str(), 0);
}
}
}
SdlLayerFactory.h
#pragma once
class SdlLayerInstanceDelegator;
class SdlLayerFactory
{
public:
static SdlLayerInstanceDelegator *getInstance();
static SdlLayerInstanceDelegator *replaceInstance(SdlLayerInstanceDelegator *replacement);
private:
static SdlLayerInstanceDelegator *instance;
SdlLayerFactory();
~SdlLayerFactory();
};
SdlLayerFactory.cpp
#include "SdlLayerFactory.h"
SdlLayerInstanceDelegator *SdlLayerFactory::instance = 0;
SdlLayerFactory::SdlLayerFactory(){}
SdlLayerFactory::~SdlLayerFactory(){}
SdlLayerInstanceDelegator *SdlLayerFactory::getInstance() {
return instance;
}
SdlLayerInstanceDelegator *SdlLayerFactory::replaceInstance(SdlLayerInstanceDelegator *replacement) {
SdlLayerInstanceDelegator *original = instance;
instance = replacement;
return original;
}
SdlLayerInstanceDelegator.h
#pragma once
struct SDL_version;
class SdlLayerInstanceDelegator {
public:
virtual ~SdlLayerInstanceDelegator(void) = 0;
virtual const SDL_version *SDL_Linked_Version() = 0;
virtual int SDL_Init(int bitMask) = 0;
virtual void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode) = 0;
virtual void SDL_WM_SetCaption(const char *title, int someIntValue) = 0;
protected:
SdlLayerInstanceDelegator();
private:
SdlLayerInstanceDelegator(const SdlLayerInstanceDelegator &rhs);
SdlLayerInstanceDelegator &operator = (const SdlLayerInstanceDelegator &);
};
SdlLayerInstanceDelegator.cpp
#include "SdlLayerInstanceDelegator.h"
SdlLayerInstanceDelegator::SdlLayerInstanceDelegator(){}
SdlLayerInstanceDelegator::~SdlLayerInstanceDelegator(){}
TestDoubleSdlLayerInstanceDelegator.h
#pragma once
#include "SdlLayerInstanceDelegator.h"
class TestDoubleSdlLayerInstanceDelegator: public SdlLayerInstanceDelegator {
public:
TestDoubleSdlLayerInstanceDelegator();
virtual ~TestDoubleSdlLayerInstanceDelegator();
const SDL_version *SDL_Linked_Version();
int SDL_Init(int bitMask);
void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode);
void SDL_WM_SetCaption(const char *title, int someIntValue);
};
TestDoubleSdlLayerInstanceDelegator.cpp
#include "TestDoubleSdlLayerInstanceDelegator.h"
TestDoubleSdlLayerInstanceDelegator::TestDoubleSdlLayerInstanceDelegator(){}
TestDoubleSdlLayerInstanceDelegator::~TestDoubleSdlLayerInstanceDelegator(){}
const SDL_version *TestDoubleSdlLayerInstanceDelegator::SDL_Linked_Version() {
return 0;
}
int TestDoubleSdlLayerInstanceDelegator::SDL_Init(int bitMask) {
return 0;
}
void *TestDoubleSdlLayerInstanceDelegator::SDL_SetVideoMode(int x, int y, int bitDepth, int mode) {
return 0;
}
void TestDoubleSdlLayerInstanceDelegator::SDL_WM_SetCaption(const char *title, int someIntValue){}
RealSdlLayerInstanceDelegator.h
#pragma once
#include "SdlLayerInstanceDelegator.h"
class RealSdlLayerInstanceDelegator : public SdlLayerInstanceDelegator
{
public:
RealSdlLayerInstanceDelegator();
virtual ~RealSdlLayerInstanceDelegator();
const SDL_version *SDL_Linked_Version();
int SDL_Init(int bitMask);
void *SDL_SetVideoMode(int x, int y, int bitDepth, int mode);
void SDL_WM_SetCaption(const char *title, int someIntValue);
};
RealSdlLayerInstanceDelegator.cpp
#include "RealSdlLayerInstanceDelegator.h"
#include "sdl.h"
RealSdlLayerInstanceDelegator::RealSdlLayerInstanceDelegator(){}
RealSdlLayerInstanceDelegator::~RealSdlLayerInstanceDelegator(){}
const SDL_version *RealSdlLayerInstanceDelegator::SDL_Linked_Version() {
return ::SDL_Linked_Version();
}
int RealSdlLayerInstanceDelegator::SDL_Init(int bitMask) {
return ::SDL_Init(bitMask);
}
void *RealSdlLayerInstanceDelegator::SDL_SetVideoMode(int x, int y, int bitDepth, int mode) {
return ::SDL_SetVideoMode(x, y, bitDepth, mode);
}
void RealSdlLayerInstanceDelegator::SDL_WM_SetCaption(const char *title, int someIntValue) {
::SDL_WM_SetCaption(title, someIntValue);
}
SdlHardwareLayerTest.h
#include <CppUTest/TestHarness.h>
#include "sdl.h"
#include "SdlLayerFactory.h"
#include "SdlHardwareLayer.h"
#include "TestDoubleSdlLayerInstanceDelegator.h"
TEST_GROUP(SdlHardwareLayerTest) {
virtual void setup() {
original = SdlLayerFactory::getInstance();
layer = new SdlHardwareLayer;
}
virtual void teardown() {
if(SdlLayerFactory::getInstance() != original)
delete SdlLayerFactory::getInstance();
SdlLayerFactory::replaceInstance(original);
delete layer;
}
SdlLayerInstanceDelegator *original;
SdlHardwareLayer *layer;
};
struct MajorMinorSdlhardwareLayer : public TestDoubleSdlLayerInstanceDelegator {
struct SDL_version v;
MajorMinorSdlhardwareLayer() {
v.major = 0;
v.minor = 0;
}
const SDL_version *SDL_Linked_Version() {
return &v;
}
};
TEST(SdlHardwareLayerTest, ItGeneratsErrorWithLowMajorMinorVersionNumber) {
SdlLayerFactory::replaceInstance(new MajorMinorSdlhardwareLayer);
try {
layer->initializeHardware();
FAIL("Should have thrown int value");
} catch (int value) {
LONGS_EQUAL(1, value);
}
}
struct InitFailingSdlHardwareLayer : public MajorMinorSdlhardwareLayer {
InitFailingSdlHardwareLayer () {
v.major = 3;
v.minor = 3;
}
int SDL_Init(int bitMask) {
return -1;
}
};
TEST(SdlHardwareLayerTest, ItGeneratesErrorWhenInitReturnsLessThan0) {
SdlLayerFactory::replaceInstance(new InitFailingSdlHardwareLayer);
try {
layer->initializeHardware();
FAIL("Should have thrown int value");
} catch (int value) {
LONGS_EQUAL(2, value);
}
}
RunAllTests.cpp
#include <CppUTest/CommandLineTestRunner.h>
int main(int ac, char **av) {
return CommandLineTestRunner::RunAllTests(ac, av);
}