It probably happened to most of us developers before: while refactoring, you change the name of a virtual method, but forget to change the name of the overriding method in one of your derived classes. Compilation works fine, all unit tests pass, but your program doesn’t work: the overriding method is never called. Java (and C#) programmers can avoid this problem by putting @Override (and override) in front of their methods, which causes the compiler to print out an error message if the method is not overriding anything. However, most other languages leave you hanging with this problem. Luckily, with statically typed languages like C++, you can avoid these bugs by slightly adapting your unit tests.
Suppose you have a base class Oracle, and a derived class GeekyOracle, defined as follows:
class Oracle {
virtual int getLckyNmbr() {
return 13;
}
};
class GeekyOracle : public Oracle {
virtual int getLckyNmbr() {
return 42;
}
};
Being a good developer, you unit test GeekyOracle’s getLckyNmbr():
void GeekyOracleTest::testGetLckyNmbr() {
GeekyOracle* oracle = createGeekyOracle();
assertEquals(42, oracle->getLckyNmbr());
}
After a while, you realize that using abbreviations in method names is actually not very good practice, so you change your method name to something more readable:
class Oracle {
virtual int getLuckyNumber() {
return 13;
}
};
However, when you compile your program now, it is behaving completely different than before, although all unit tests passed. It requires manual debugging to find out that you forgot to rename GeekyOracle’s method as well, causing Oracle’s getLuckyNumber() to be executed, no matter what oracle you instantiate. Your unit tests didn’t catch this, because they test every method of a class separately, but don’t test the class being used in your application.
A way to check for this in your unit tests is to make sure that, when testing a virtual method, you always call it on an object that is statically typed with its base class. In our example, our unit test would call getLckyNmbr() on an Oracle instead of on a GeekyOracle:
void GeekyOracleTest::testGetLckyNmbr() {
Oracle* oracle = createGeekyOracle();
assertEquals(42, oracle->getLckyNmbr());
}
This unit test will fail to compile when you change the method name of the base class, which avoids you end up with the silly bug in your program.
If your test allocates your object under test on the stack (e.g. for simplicity), you could use the following alternative:
void GeekyOracleTest::testGetLckyNmbr() {
GeekyOracle oracle;
assertEquals(42, ((Oracle*) &oracle)->getLckyNmbr());
}
However, since this degrades the readability of the test, I prefer to add a static helper method to my unit test, and write the test as follows:
static inline Oracle* p(Oracle& foo) {
return &foo;
}
void GeekyOracleTest::testGetLckyNmbr() {
GeekyOracle oracle;
assertEquals(42, p(oracle)->getLckyNmbr());
}
Although I used C++ in my example, this technique should work for all statically typed languages. Advocates of dynamically typed languages claim they can compensate the lack of types by unit testing their classes properly; I wonder how they avoid this kind of problems, though.
Tags: C++, Programming, Unit Testing