Even Bad Unit Testing Helps
I remember the first time I used unit testing. It was my honours year project. One of my friends had recently started a job and told me about this testing technique I had never heard of. I decided to give unit testing a shot for my project.
Since it was my first attempt, my tests were terrible. I made a lot of beginner mistakes:
- writing all the tests for a class before implementing it
- missing out important edge cases
- never refactoring the production code
- never refactoring the test code
- unreadable tests
- testing after I wrote the production code instead of first
- not testing a piece of code if it was too difficult
- obsessing over testing null arguments
- writing fragile tests that broke frequently
- using reflection to test private methods
- fixing bugs without a failing unit test
My production code was awful too:
- huge methods
- tight coupling
- circular dependencies
- business logic in the user interface
- deep inheritance trees
Despite this mediocrity, I managed to finish the project. Not only that but it was the biggest project I had completed at the time. I credit being able to finish to unit testing.
In previous projects, once the codebase reached a certain size, bugs would start cropping up. I'd fix a bug and my fix would cause another bug to appear somewhere else in the code. Over time, bugs I had seen before would reappear. It was like whack-a-mole with bugs!
This project was different. When I fixed a bug, there would still be a knock-on effect. More often than not, my test suite would alert me to the new bug and I could fix it immediately. The benefit the tests gave me was to stop bugs propagating through my crappy code. Instead of the growing number of bugs limiting me from progressing, I was able to keep my code working as I continually added features.
In retrospect, this propagation of bugs was due to crappy design. Changing an abstract class near the root of a large inheritance tree can cause all sorts of side effects in the leaf nodes. Changing a widely-invoked method can adversely effect several of its clients. Circular references lock classes in mutual dependency where bugs ping-pong between them as I fix them.
These things are all examples of tight coupling. Many of my tests not only tested the method they called but also that method's dependencies. This way when I changed some code that caused bugs elsewhere, I probably had a test that would pick up that effect.
The take home message is that you don't need to be great at unit testing to get tremendous benefit from it. Even these bad tests helped me to prevent regression and allowed me to complete the requirements.
01 March 2017