I’m probably horribly late for the party, but I’m currently exploring test-driven development after reading Kent Beck’s classic Test-driven development: By example. Although most developers claim to know TDD, I have only met a few who knew what it was actually about – the majority seems to confuse it with unit testing.
Here’s the surprise: TDD is not a testing technique. It’s a development technique, and a good one at that. Here’s why I like it:
- It forces you to carefully think about what you want to implement before you dive into the how, leading to simpler APIs.
- It pushes you towards loose coupling and modularisation – virtues in object-oriented design.
- It makes you focus your energy on solving actual problems, away from just-in-case code and bloated, prophetic designs.
This is how TDD works in a nutshell:
- Think about what you want to create and hence what you want to test. Make a TODO list of all the test cases you plan to write.
- Pick a test case and implement it. Use imaginary classes and methods – it doesn’t have to compile.
- Make the test case compile as fast as possible. Create empty classes, implement methods that return constants, just make it compile.
- Run the test case. It will most likely fail. Now make it pass as fast as possible, using constants and duplication is explicitly allowed.
- As soon as the test case passes, refactor the code. Remove constants and duplication, refactor your dreamt API to match reality, write Javadoc etc. Run all test cases often and make sure that they still pass once you’re done. If you find a problem that isn’t identified by the existing test cases, add it to the TODO list and ignore it for now.
- Pick the next test case to implement. If you are unsure whether the code will only work with the specific test case you just wrote, write a similar one with different data. Beck calls this triangulation and it is supposed to increase the confidence in your code. TDD is all about confidence.
I’ve investigated TDD with two problems so far:
I first tackled an interview-style question about combinatorics. Although I was not a hundred percent sure what I was doing at all times, I was able to create a working solution astonishingly fast. Whenever I was in doubt (quite often), I would write another test case and fix the code until all test cases passed again. Now I know why it’s called test-driven; it does indeed drive development, e.g. by breaking down complicated problems into smaller, solvable ones. I never had to stop and agonise – if I couldn’t make a test case work, I threw it away and wrote one that made a smaller step towards my goal.
That really aroused my interest in TDD, but it was a neat little isolated problem, something for which TDD is known to work very well. How about a more realistic problem?
I recently started to port a Tetris clone I wrote in Java to GWT. There were performance problems because I drew the whole grid again after each change instead of just reacting to the changes, so I decided to rewrite the game logic. I figured that this would be just the right problem for my next TDD experiment, plus I was curious how it would fare in game development, so I began.
I just implemented the last test case from my list and I’m pretty happy with the results. However, it was more complicated than my first experiment, and there were many design issues. For instance: In the game (I assume you know Tetris), new pieces are placed at random horizontal positions. I could safely ignore this fact for a while, but as soon as I had to write test cases for moving pieces horizontally, things got complicated.
My test cases asked the pieces for their horizontal position and tested movement based on that information. I asserted that the moved piece did move left, unless it was on the left edge of the grid, in which case it shouldn’t move. I noticed that this test case would sometimes pass and sometimes not, based on the random horizontal position of the piece (e.g. if my code was broken and didn’t move the piece at all, the test case would only pass if the piece was positioned on the left edge of the grid).
I was not willing to tolerate a test case that would sometimes pass and sometimes not, because I wouldn’t be able to have confidence in the test cases anymore. Should I run all test cases a hundred times in a row after each change, just to make sure that each situation is tested? I had to get the randomness out of the game logic code. I did that by creating a class that would generate random numbers with methods like createRandomPiecePosition(). I then created an interface for this class and a mock implementation that returned fixed values which could be set by the test cases as required. This solved the problem, and it’s really nice in terms of modularisation.
I really enjoyed refactoring whenever I felt like it – rerunning my test cases gave me confidence that I didn’t break anything. I also noticed that I was surprisingly fast, even though I spent a lot more time on design issues than anticipated, because of the fast workflow: I could write some code and instantly see whether it works. When I worked on the Java version of the game, I had to create a specific situation (e.g. game over) by actually playing the game, which was annoyingly time consuming. Furthermore, the more unexpected bugs I fixed, the less confidence I had in the code, making me test more often. With the TDD workflow, I noticed an unexpected bug, wrote a test case to identify it and made it pass. I never had to think about the bug again, because I knew that there was a test case that would identify it should it occur again.
All in all, I’m really impressed by TDD, and I plan to use it for the game logic of my latest project.