Skip to main content

Test driven

Since half a year, I'm reading a lot about "modern" software engineering approaches, like agile development practices, XP and test-driven development. Read on for a short story how I work today.

This is how I write my code currently, and I'm pretty productive that way, but your mileage may vary.

Usually, I'm working in short sprints nowadays, I try to keep those sprints around 2 hours. Before I start a sprint, I think about what I want to have at the end, usually some specific functionality. It's just a rough idea what should work, no concept paper, though I tend to write up a short note before implementing a larger system, covering the main classes and their responsibilities.

At the beginning, I just write a skeleton class header and define the types I think I'm gonna need, and the helper objects definitions, just enough that it compiles. There is no code that does something, so this is usually really quick (like 5 minutes or so). Note that I don't implement the functions so calling them will result in a link error.

Then, I start to write unit tests for helper classes, one for each function, of course only if there are helper classes that I'm aware of beforehand (often, the helper classes appear during a later refactoring). This takes me around 10 minutes or so per function, including the unit test. If I end up using some other function, I'm getting a linker error so I can see if I called the function properly or whether I thought the syntax is different (this happens most of the time - you have a function called doX in your interface but in your test case you think it's called getQ, this is your chance to fix this, most of the time you were wrong when defining the interface), and I can go ahead and implement the missing ones right away. I check in the code at each step, meaning once after the interface, then after each test, then after implementing the function, and then after some bug fixing or test expanding.

I'm trying to keep the tests simple, just a few lines, for most helper classes, they are 2-3 lines long at most. I try to test border cases, too, it's quite natural to have one test like "PopStack" and write a "PopEmptyStackThrows" just along with it to see what happens. Here, a crash costs you maybe a minute to fix it, later, you could end up spending an hour tracking it down. And for some reason or another, it is fun to test such border cases :)

After the functionality is pretty much done, I do a refactoring pass. During the implementation, I often end up with duplicate code, and in this step, I try to clean it up as much as possible. With the unit tests in place, I can be pretty sure that I don't break stuff in this phase. I add a few comments now for stuff that is not obvious. Basically, the goal is to have solid and polished code that is suitable for further development, but it's not top-notch as it has been just used by the unit tests mainly and not by "real" code.

As soon as the helper classes are in place, I start with the main class. I try to focus on a single function, just as with the helper classes, and get this working. I try to have at least two tests for each function, one where it works, and one which shows what happens in case of an error. The latter one is actually the more important one, as it is the one which makes the code robust, as you tend to forget what will be treated as an error usually. It's actually just the same as with the helper classes. I tend to go bottom up by using only functionality that I have tested already. This means usually to start with the simplest functions first and work towards the complex ones. If I need a new helper function, I test it first before using it somewhere else, so I have less trouble if a test fails.

The last step - as soon as the whole class works - is yet another refactoring step to consolidate functionality and extract utility functions. In this step, I usually polish the docs, document invariants, and add further checks for error cases (with tests). After this step, I consider the code production ready and move on - the code in this stage has each function documented, with pre/post conditions, unit tests for each function, has been refactored once or twice, and is of pretty high quality.

In case I discover a bug later (by other code that depends on this class or functionality), I always write a test case for the affected class first (and check it in) before fixing it. I also look through all similar code to see if some other part is affected by the same bug (this is usually a sign that some refactoring is needed).