I think there is some general confusion about the benefits of Test Driven Development (TDD) but also when it's applicable. I'm very much of the opinion that it doesn't always lend itself to everything you build. Now I'm expecting a slight backlash on this but I believe its true. You should always test your code and have a suite of tests for the functionality once it's been built but to do "pure" TDD isn't always needed.
Why do developers not unit test?
Over the years I've seen a number of developers not unit test for various reasons. The excuses range from not seeing the benefit, being too hard, not understanding how etc. If testing is hard to do developers will not do it. Another reason is that the code written is too complicated and require more setup to work. This is one of the arguments for implementing TDD as it will make the dependencies and complexities more obvious as more development is done. However if you keep an eye on these complexities and keep testing then it can still be tamed without pure TDD.
Don't get me wrong, TDD is hard to get right!
My favorite phrase I've adopted over the years is "it depends". Coming back to "does it always apply?" is the same ... "it depends". TDD can be hard to do if the requirements are not fully defined at the start or functionality evolves over time.
Single Responsibility Principle
Testing and TDD is clearer if what you are building is well defined and clear. You can test more easily if you apply the Single Responsibility Principle (SRP) to your code. It's all too easy to just add another if statement or add further processing. It's seen as easier or less work than refactoring the code into separate classes/functionality areas but one of my general rules of thumb I work on is if the Arrange setup of your test is too complicated or elaborate then it's time to refactor. This for me is a code smell that your code is doing too much and ultimately violating the SRP. It's also short term gain for long term pain. You will likely refer to the code you have just written as "technical debt" within a few weeks or months if you don't do something about it.
Open / Closed Principle
This leads us into another of the SOLID principles; the Open / Closed Principle. If the code is structured well enough then the code can be open for extension but closed for modification. But what does that mean? When we refactor something and keep our tests green we need to allow for extension. An example of applying the Open / Closed Principle is when you need to add a new item to a switch statement it could be a code smell. You are modifying the code but not extending it. This is the point where you need to look at potential design patterns and refactor. Early on in my career I thought adding more classes and moving code around was a bad thing and this concept could be stopping less experienced developers struggle with TDD.
Unit tests at the end of the day are there to help build confidence in your code. It helps make you happy that something else won't break if you change something. It allows for quick changes and quick deployments with a high level of confidence in your code. This however only works if you have good tests!
TDD and Defect Resolution
If you have a good suite of tests it can aid with proving defects and ultimately resolving them through TDD. This allows for a high level of confidence that you are not only fixing the issue and improving the quality of your code but also that you're not adding more regression issues into the functionality.
In one of my projects recently I have been creating tests to prove any defects which have been raised are actually a bug and then using them to make the change and check the functionality. If another test breaks you know you've not quite got it right. The names of the tests relate to the defect numbers and can be followed back to the business "why" of the issue. Once the changes have been made and the bug is fixed there is now improved testing coverage which cover further use cases and regression testing. This in itself is good but it also then allows for further refactoring if required and the edge cases are still covered.
Personally TDD lends itself to defect fixing and further edge case development coverage on existing functionality very well and I will continue to push with this approach moving forward.
Don't get me wrong the mentality of applying TDD can mess with your head. Applying "only enough code for tests to pass" and starting by hard coding "return true", which is the common example, goes against our ingrained thinking as developers. Stick with the "enough code to pass the tests" mantra and you should be well on your way to leaner code.
Less code should lead to less bugs.
What are your thoughts on practical TDD? Let me know on Twitter @WestDiscGolf