I’m merely reiterating what Scott has already said in the podcast, but it’s different when I can personally say that I’m joining the ranks of countless others who have seen the benefits of TDD.
Now I can’t say that I’m a 100% TDD practitioner since I’m still learning how to write tests before code effectively, but I’ve already seen the improvements in design in my code many times over just by merely writing unit tests. At this point I'd say half of my tests are written first, and the other after are after the fact.
I’ve been through many phases of writing unit tests, it it took me a long time to get it right (and I’m sure I have tons more to go). It takes some experience to figure out how to properly write unit tests, and as a by-product how to make classes easily testable. Code quality and testability go hand-in-hand, so if you find something difficult to test, it’s probably because it was badly designed to start.
The first time I was introduced to unit testing in a work setting was when I was writing C++…and man it was ugly, not because it was C++, but because everything was tightly coupled and everything was a singleton. The fixture setup was outrageous, and it was common to ::Sleep(5000) to make tests pass. Needless to say, my first experience with unit testing was extremely painful.
After a job switch and back to the C# world, I started reading more blogs, listening to more podcasts, and experimenting more. I was given a project to prototype, and for phase 1 it was an entirely DDD experience with I got the opportunity to experiment with writing unit tests the “proper” way with dependency injection and mocking frameworks.
Unit tests are easy to write when you have highly decoupled components.Prototype was finished, and now I was back on the main project, and given a critical task to complete in 2 weeks. I had to modify a class which was 10,000 lines long, which has mega dependencies on everything else in the project. Anything I touched could potentially break something else. And of course…no unit tests whatsoever. I like challenges and responsibility – but this was close to overkill. This thing produces revenue for the company daily so I really don’t want to mess anything up.
First thing I realized was that there’s no way I could possibly write any unit test for something like this. If the class file was 10,000 lines long, you can imagine the average line count for methods.
And of course, the business team didn’t make things easy on me by asking that this feature be turned off in production, but turned on for QA. So, the best option was to refactor the existing logic out to a separate class, extract an interface, implement the new implementation, and swap between the 2 implementations dynamically.
After 4 days of analyzing and reading code to make sure I have a very detailed battle plan, I started extracting the feature to a new class. The first iteration of the class was UGLY. I extracted out the feature I was changing to a separate class, but the method names were archaic and didn’t have any good “flow” to them. I felt that I had to comment my unit tests just so whoever’s reading them could understand what’s happening, which brings up a point.
If you need to comment your unit tests, you need to redesign the APIIt took many iterations and refactoring of the interface to get it to the point where I found acceptable. If you compared the 1st batch of unit tests to the last batch of unit tests it is like night and day. The end result were unit tests which typically followed a 3-line pattern of setup/execute/verify. Brain dead simple.
The last thing to do was to reintegrate the class into the original class. For this I was back to compile/run/debug/cross-fingers, but I had full confidence that whenever I called anything in the extracted class it would work.
An added benefit is that I didn’t add another 500 lines of code to the already gigantic class. Basically, in summary:
- Get super mega long legacy code class files
- Extract feature into separate file as close to original implementation as possible (initially will be copy-paste)
- Run and make sure things don’t die
- Write unit tests for extracted class (forces you to have a better understanding of the code modifications, and the requirements)
- Make sure unit tests cover all possible scenarios of invocation from original class
- Start refactoring and redesigning to better testability, while maintaining all previous tests
- Done! Repeat as necessary!