In my travels I'm sometimes asked to help a client write some unit-tests for C/C++ code which wasn't built with unit-testing in mind and so, naturally, is resisting being unit-tested.
This is a classic chicken and egg situation;
you want to refactor the code to get the unit-tests in place, but of course that's dangerous and painful and slow until you've got at least some unit tests in place.
There are two big problems:
- Repaying the legacy debt is likely to be a long and arduous road.
There's not a lot I can do to help here except offer encouragement
and to maybe remind them of the Winston Churchill quote
if you're going through hell, keep going!
- It often seems there's no way to get started.
Clients might say something like "this can't be unit tested"
when of course what they really mean is "I don't know how to unit test this".
Sometimes I can suggest tricks and techniques.
One way to get started is to make the problem smaller.
Suppose I have a large legacy C++ class resolutely resisting being unit-tested.
I pick a method and start with that.
For example, given this file, fubar.cpp
1|#include "fubar.hpp"
2|#include ...
3|#include ...
|...
1438|int fubar::f1() const
1439|{
| ...
1452|}
1453|
1457|void fubar::example(widget & w, int x)
1458|{
| ...
1598|}
1599|
1600|int fubar::f2()
1601|{
| ...
4561|}
4562|
I decide to start with
fubar::example()
which starts at line 1457 of
fubar.cpp
and ends 100+ lines later:
I carefully
cut all of lines 1457-1598 into its own new file called
fubar-example
1|
2|void fubar::example(widget & w, int x)
3|{
| ...
141|}
142|
and replace the cut lines from
fubar.cpp
with a single
#include
to
the new file:
1|#include "fubar.hpp"
2|#include ...
3|#include ...
|...
1438|int fubar::f1() const
1439|{
| ...
1452|}
1453|
1457|#include "fubar-example" // <----
1458|
1459|int fubar::f2()
1460|{
| ...
4420|}
4421|
I'm aiming to create a unit-test for
fubar::example()
like this:
// here I'll dummy out everything used in fubar::example
#include "fubar-example"
// here I'll write my first unit test
However, as safe as it seems, this could cause a change in behaviour!
I can easily check this. If
fubar.cpp
is one of the source files that
compiles into
something.lib
then I can compare the 'before' and 'after'
versions of this lib file to see if they are identical. They should be.
One reason they might not be is because of things like the
assert
macro
which uses __FILE__ and __LINE__ to report the filename and line-number.
I've changed the line numbers on everything below the new
#include
and the lines numbers and
filename inside the included file.
I can fix that using the
#line
directive.
In the original
fubar.cpp
file
example()
started at line 1457 so
fubar-example
becomes:
1|
2|...
3|
4|#line 1457 "fubar.cpp" // <----
5|void fubar::example(widget & w, int x)
6|{
| ...
145|}
146|
and the next method
f2()
started at line 1600 so
fubar.cpp
becomes:
1|#include "fubar.hpp"
2|#include ...
3|#include ...
|...
1438|int fubar::f1() const
1439|{
| ...
1452|}
1453|
1457|#include "fubar-example"
1458|
1459|#line 1600 // <----
1460|int fubar::f2()
1461|{
| ...
4421|}
4422|
Now the before and after versions of the lib file are identical.
Now I try to compile the test file:
// here I'll dummy out everything used in fubar::example
#include "fubar-example"
// here I'll write my first unit test
It fails to compile of course, since I can't define
fubar::example()
unless I've previously declared it. So I dummy it out:
class fubar
{
public:
void example(widget & w, int x); // <----
};
#include "fubar-example"
// here I'll write my first unit test
Now it fails because the compiler doesn't know what
widget
is.
So I forward declare it:
class widget; // <----
class fubar
{
public:
void example(widget & w, int x);
};
#include "fubar-example"
// here I'll write my first unit test
Now it fails because
fubar::example()
calls a method
nudge(int,int)
on the
widget
parameter:
1|
2|...
3|#line 1457 "fubar.cpp"
4|void fubar::example(widget & w, int x)
5|{
| ...
| w.nudge(10,10);
| ...
145|}
146|
So I dummy it out:
class widget
{
public:
void nudge(int,int) // <----
{
}
};
class fubar
{
public:
void example(widget & w, int x);
};
#include "fubar-example"
// here I'll write my first unit test
Now it fails because
fubar::example()
invokes a macro
LOG
:
1|
2|...
3|#line 1457 "fubar.cpp"
4|void fubar::example(widget & w, int x)
5|{
| ...
| LOG(... , ...);
| ...
145|}
146|
So I dummy it out:
#define LOG(where,what) /*nothing*/ // <----
class widget ...
class fubar
{
public:
void example(widget & w, int x);
};
#include "fubar-example"
// here I'll write my first unit test
Maybe later I can return to the dummy
LOG
macro
and make it less dumb but for now I'm not even compiling. One thing at a time.
Now it fails because
fubar::example()
declares a local
std::string
:
1|
2|...
3|#line 1457 "fubar.cpp"
4|void fubar::example(widget & w, int x)
5|{
| ...
| std::string name = "...";
| ...
145|}
146|
This one I don't need to dummy out.
#include <string> // <----
#define LOG(where,what) /*nothing*/
class widget ...
class fubar
{
public:
void example(widget & w, int x);
};
#include "fubar-example"
// here I'll write my first unit test
Now it fails because
fubar::example()
calls a sibling method:
1|
2|...
3|#line 1457 "fubar.cpp"
4|void fubar::example(widget & w, int x)
5|{
| ...
| if (tweedle_dee(w))
| ...
145|}
146|
So I dummy it out:
#include <string>
#define LOG(where,what) /*nothing*/
class widget ...
class fubar
{
public:
void example(widget & w, int x);
bool tweedle_dee(widget &) // <----
{
return false;
}
};
#include "fubar-example"
// here I'll write my first unit test
Now it fails because
fubar::example()
makes a call on one of its data members:
1|
2|...
3|#line 1457 "fubar.cpp"
4|void fubar::example(widget & w, int x)
5|{
| ...
| address_->resolve(name.begin(), name.end());
| ...
145|}
146|
So I dummy it out, making no attempt to write the actual types of the parameters (a useful trick):
#include <string>
#define LOG(where,what) /*nothing*/
class widget ...
class address_type
{
public:
template<typename iterator>
void resolve(iterator, iterator) // <----
{
}
};
class fubar
{
public:
void example(widget & w, int x);
bool tweedle_dee(widget &)
{
return false;
}
address_type * address_; // <----
};
#include "fubar-example"
// here I'll write my first unit test
On I go, one step at a time, until finally,
it compiles! Hoorah!
I'm reminded of a saying I heard (from Michael Stal).
It's when there's a library you pull in but, on pulling it in, you find it has two
further dependencies and you have to pull in those aswell. And they have their dependencies
too. etc etc. The saying is:
you reach for the banana; you get the whole gorilla!
Except that sometimes it's worse than that. Sometimes...
you reach for the banana; you get the whole jungle!
Ok. So now it compiles.
But I haven't written my first unit-test yet!
So I start that:
#include <string>
#define LOG(where,what) /*nothing*/
class widget ...
class address_type ...
class fubar
{
public:
void example(widget & w, int x);
bool tweedle_dee(widget &)
{
return false;
}
address_type * address_;
};
#include "fubar-example"
int main()
{
fubar f;
widget w;
f.example(w, 42); // <----
}
Now I have a test I can actually
run!
Hoorah!
There's no actual assertions yet, but one thing at a time.
I run it.
It crashes of course.
The problem could be the
address_
data member. The compiler
generated default constructor doesn't set it so it's a random pointer.
I might be able to fix that by repeating the same extraction of the constructor(s)
into separate #included files. Ultimately that's what I want of course.
But one thing at a time.
I can definitely fix it by writing my own constructor:
#include <string>
#define LOG(where,what) /*nothing*/
class widget ...
class address_type ...
class fubar
{
public:
explicit fubar(address_type * address) // <----
: address_(address)
{
}
void example(widget & w, int x);
bool tweedle_dee(widget &)
{
return false;
}
address_type * address_;
};
#include "fubar-example"
int main()
{
address_type where; // <----
fubar f(&where); // <----
widget w;
f.example(w, 42);
}
Now it compiles and
runs without crashing! Hoorah!
It is a horrible hack. Painful.
But the gorilla is no longer so invisible!
And if nothing else, I've got code reflecting the current understanding of my attempt to hack a way into the jungle!
I've made a start.
I've got something I can build on.
And remember, when you say "X is impossible" what you really mean is "I don't know how to X".
P.S.
Here's another
horrible testing hack for C/C++.