One of the questions people that want to start with Test Driven Development ask regularly is: “I find starting with TDD on an existing project hard. How do you get old legacy code testable?”. Unfortunately, one of the most common answers to that question is: “You just have to begin and take small steps.” And while the answer has truth in it, it’s also a much too simple answer that doesn’t really help you to start. How come and what is a better answer to the question on how to start?
One of the many things I like about automatic tests is that tests reveal mistakes. Revealing mistakes on behavior is obvious: when the code you exercise produce a different result than the result you expect, the test fails. But automatic tests also show you another kind of mistakes: design flaws. If you want to test some code but you can’t write a test for it because it’s impossible to isolate the part of the code you want to test (due to dependencies, couplings etc.), that’s due to a design flaw. Therefore, what makes TDD hard in an existing project is not TDD itself, but the Technical Debt you have to combat.
If you want to start writing new functionality using TDD on an existing code base and you find it hard to do, you should be aware that this is mostly a Technical Debt issue. Therefore, you don’t only need to be proficient in writing great tests and let them drive your development. Another thing you need to know is where you want and need to go in terms of a clean design and structure of your code that allows for easy testing. You also need to know how you can safely make small adjustments to the structure and organization of your code to get to that new design. Finally, you (and all others working on the same product) need to have to proper mindset and attitude to make it a success.
You need to know design principles like SOLID, DRY, YAGNI and design patterns to know where you need to go with your design to have low-coupled and well-structured code with that’s easy to test. Object Oriented analysis and design is hard. To have high quality code, you don’t only need to know common design patterns and principles, but you also need to know which problem they, and why and how they work. Then you need to recognize code smells in your code and know if and how a pattern can help you to get higher quality code that is testable.
Then there’s the practice of getting from a bad situation to a better situation in a structured way. Refactoring is the act of making changes to the internal structure of software to make it easier to understand and change without changing its behavior. In TDD, refactoring is done continuously and you have tests that help you to safely do it. But with legacy code, this probably isn’t the case (otherwise you wouldn’t be in this situation) and the chunks that needs to be refactored are larger. You must recognize common problems in your code and know what refactoring patterns you can apply to safely and incrementally clean up your code and bring it under tests. It really helps if you know how you can leverage your IDE to help you with these refactorings. All modern IDE’s like IntelliJ, Eclipse and Visual Studio (especially with the Resharper plugin) have functionality to (partly) automate this while preventing unwanted changes in the behavior of the application.
But in the end, it all comes down to mindset and attitude. Implementing a feature by just adding one line of code to an already huge method may be very tempting, especially when solving it the proper way takes a lot of time. In software development, this is sometimes called the Broken Window Phenomenon. Be aware that this probably is what caused you to be in the situation that you’re in today. If you want to change this, make an explicit decision to not continue down this path and invest in creating a better code base.
To summarize: if you want to really be effective in practicing TDD in an existing project and getting your legacy code under tests and cleaned up, you need more than just TDD. You need to:
- Be proficient in TDD and
- Be proficient in recognizing and applying OO Design Principles and Patterns and
- Be proficient in refactoring code and
- Have the proper professional mindset and attitude
To start: make TDD part of your way of working, use it to develop new functionality and invest time in becoming better and better in design, refactoring and TDD. Be aware of the combination of multiple skills needed to make it a success. Finally, accept that it is hard and don’t take shortcuts!
This blog was written by Harm Pauw.