A while ago I technically screened a developer for one of our biggest client. This developer was a huge fan of unit testing. Whatever software puzzle I submitted could be solved with an unit test.
Here is a sample of the interview:
Let's sum this up with a chart:
No need to say that the person wasn't hired. What I realized by then is that unit testing was not only often misunderstood but overrated. I don't imply we ignore unit testing at Bureau 14, we just have some degree of moderation in their usage...
Let's say we want to implement a string library in C++ for some reason. I'm talking something very simple with interfaces similar to the STL string. We're going to call it xt_string.
Test driven development is about writing tests before writing code. If you ask me, when you write the tests is not that important. I'd even say that writing tests before writing code will make you believe you don't need to write specifications or documentation (agile or not, write specifications, thanks).
Nevertheless, we're going to write obvious tests that I often call "space continuum integrity tests". Basically when these tests fail, something is terribly wrong either with your code or the physical laws of the universe. Generally, it's a problem within the code.
1
2 3 4 |
xt_string st1
;
BOOST_CHECK (st1. empty ( ) ) ; xt_string st2 ( "test" ) ; BOOST_CHECK_EQUAL (st2, "test" ) ; |
Because we care about performance, our xt_string uses custom allocated buffers aligned on the cache. And lucky us, we have a function to validate that! Quick, to the unit tester!
1
2 |
xt
string st("oh my"); BOOSTCHECK (is cachealigned (st. buffer ( ) ) ) ; |
The buffer method returns the underlying buffer, as you probably inferred.
This test is horribly bad. Horribly. Why? Well because it's in "The bad unit test" section for instance.
It seems clever at first, but you're shooting yourself in the foot with a bullet that travel through the future. You pull the trigger now and your foot explodes one year later.
We have a simple policy here: each unit test must be ignorant of the internals. In other words, we only do black box unit tests.
Here is a non-exhaustive list of reasons :
In short: a good unit test validates features and their implications without assuming anything about the implementation.
So that's all there is to a good unit test? Actually, there's more to it.
Let's have a look at this:
1
2 3 4 5 6 7 8 9 10 11 12 |
xt
string st1("yes"); xtstring st2 ( "no" ) ; BOOST CHECKEQUAL (st1, "yes" ) ; BOOST CHECKEQUAL (st2, "no" ) ; st1 = st2 ; BOOST CHECKEQUAL (st1, st2 ) ; xt string st3("maybe"); BOOSTCHECK (st3, "maybe" ) ; st2 = st3 ; BOOST CHECKEQUAL (st2, st3 ) ; BOOST CHECK(st1 != st2); BOOSTCHECK (st1 ! = st3 ) ; |
Looks pretty much like an obvious test of affectation and equality, isn't it? Except that it's more than that. This also tests the underlying memory management. What's good with this test is that if you're unifying your string buffers somewhere in the future, it's going to make sure that copy on write works.
Of course this example is far from complete and much more should be written to have some reasonable degree of validation.
Whatever implementation of strings you chose to have, the above test must remain true. You can rewrite the string class from scratch, you won't have to change the test. It's obvious, easy to understand and detects side effects.
A good unit test must have a certain degree of extra-lucidity: ability to detect future issues is the hallmark of a good unit test.
Think about what you might do with the tested code in the forthcoming years and you'll write good unit tests.
The real benefits of unit testing comes when you validate the interactions of your different structures and functions.
Ideally, your unit tests should reflect what the users are going to do with your program. Don't try to have 100 % coverage immediately (or ever actually). Instead, aim for all typical usage scenarii. That means that after the build, when the tests are run, you know your program won't crash immediately or spit blatantly wrong results. This doesn't replace integration testing and regression testing, but it gives a quick feedback about what you're doing.
The second good reflex is that whenever a bug is found and reproduced, write an unit test that exhibits the problem and then fix it. In doing so you reduce the probability of getting the same bug twice to almost zero.
Passing unit tests doesn't mean your program works. Never overestimate the reliability and coverage of unit testing.
Most of all, never forget that unit testing is here to increase software quality and save time. Never put your team in a position where writing tests takes too much time from designing news features and fixing bugs.
Your customers won't like it.