Don't just fix coding defects. Follow TDD when fixing them.
It's very common that you see a bug report and you experience a "de ja vu" - "Didn't I already fix that some time ago?" Repeating bugs are very common, and a mechanism is necessary to avoid them.
TDD (Test Driven Development) is a next advancement on top of writing unit tests.
Firstly, you need unit tests for defects to ensure that they never come back. It's very likely that you, as a developer, at least once asked yourself - "Didn't I already fix that some time ago?". Well, you did, but probably you didn't put a mechanism in place which protects the system from repeating the same bug. After several changes, and probably after fixing something else, the defect from the past can repeat accidentally.
If unit tests covering defective scenarios are in place and they get executed regularly (hopefully as part of Continuous Integration), then chance of repeating bugs is technically non-existent. There is a chance that somebody will unintentionally introduce the same code issue, but the unit tests covering that scenario will fire red. So, you can sit back and relax - you are covered.
TDD is the next step, and it fits very well specifically in scenarios such as fixing defects. It helps ensure that you did fix something, and it's not just a naive belief that the code change did the needful correction. Yes, even without writing the unit test first, you can run the program and see the bug gone, proving your code change did work... but what if your change required not a single line but at least couple of those - do you really know what exactly fixed the issue? are you sure you didn't add a "dead" code? [Dead code is the code which doesn't really help anyhow with the defect, so it's going to hang there forever, doing nothing except giving you a false satisfaction.] Now, if you are not doing TDD but are doing unit tests, you can write the unit test after the code is written, but are you sure that this unit test will fail if the defect repeats?
If you write this same unit test before the actual code change, you will see it failing and then becoming green due to your changes - that is the strongest proof that all the mentioned goals are achieved: guaranteed minimal code to solve the defect, guaranteed red unit test if the defect repeats, guaranteed full coverage of all the code written, and no dead code at all.
My diagram above suggests a step "Investigate and find defective code block", which will involve debugging the application's code by running it. This is a tedious process in many cases, which involves going through several steps and screens in the application, finding the right data that helps you do so, etc. There must be a simpler way... maybe we can write the failing unit test without running the whole application?
Well, unfortunately, with the unit level role of unit tests, it will be like pointing fingers at all directions blindly. After all, in the complex codebase, many things can cause the same negative effect seen on the screen or in the system's behavior. Too much to guess, or too many units to test for the single defect.
That being said, theoretically, it's still possible to write a test which fails without identifying the exact defective unit. But that won't be a unit test.
Quick intro to acceptance tests: Highly organized agile development teams often practice writing different kinds of tests, among them - acceptance tests (terms may vary from author to author). Simply speaking, developers write tests expressing each acceptance criteria of every story. To express the criteria (because it's at a higher level), often unit level tests won't be enough, and construction of objects from several layers will be necessary. Integration edges such as databases, screens, or external services may be mocked. Purpose is to execute the test and ensure that the acceptance criteria is met by the application's code.
Given that the team practices acceptance tests, it's possible to skip the aforementioned step from the diagram. Instead of running and debugging the application, we can simply write a new acceptance test - the bug's expectation statement being the acceptance criteria it entertains. Assuming that this new acceptance test is written correctly, it should fail (because the bug is still reproducible).
We sill need to debug the code now, but instead of running the whole application, we can debug just the path of this specific acceptance test (running and debugging tests is faster than doing the same for applications, because you don't need to do those tedious things for acceptance tests, thanks to mocking and other test code techniques).
After we've found the defective code block, we can make the acceptance test pass by fixing the code block.
Just for the completeness, I need to say that acceptance and unit tests are not mutually exclusive. You can still write unit test after finding the defective code via acceptance test. Once you do that, you will have two failing tests - acceptance test and a unit test. Now you can fix the code and both will pass.
If you want to learn more about unit testing and TDD, read another article on the topic. Also, consider taking a Test-First training course for your entire team, delivered by myself.
Author of the above content is Tengiz Tutisani - owner and technical leader at tutisani.com.
If you agree with the provided thoughts and want to make it part of your team's culture, we can help.
We provide in-person, immersive technical training courses around Software Architecture, Domain-Driven Design, and Extreme Programming topics. We also develop software solutions.