Whether or not the culture in your organization is willing to accept that defects are not inevitable, you still need to increase the knowledge in your organization about defects; how they are discovered, managed, and fixed.
- Writing unit tests–A unit test is an automated way to verify and validate a specific piece of code. Each unit test should be able to run independently from any other unit test. Start having your developers write unit tests and make sure that they have enough time to write them. Run unit testing against your code before adding it to a build, and then again as part of the build. Teams that have a good suite of unit tests in place are more comfortable making changes to their code, accepting greater challenges, and so on, because they have the confidence that they can do so without adding a significant amount of debt to their backlog.
- Writing acceptance tests–An acceptance test is an automated way of validating that all of the work defined by a story is completed. Each acceptance test should be written so that the outcome is pass or fail. Start having your testers write acceptance tests and make sure that they have enough time to write them. Run acceptance testing immediately after the build has finished.
- Writing testable code–After your team is practicing unit testing and acceptance testing, then start applying what you have learned towards writing testable code. The goal should be that all code is testable by using unit testing and acceptance testing. The result will be code that has fewer defects.
- Doing test-informed development–How do you say what you want to do before you do it (and in a way that you can test to see if you did it the way you wanted to do it when you are done)? Are you writing unit and acceptance tests that help inform how you will design a feature? Are you considering the behavior of the user in that design? Instead of designing your software before you test it, figure out what the tests should be, implement the tests, and then do the design. Don't spend your time in the debugger; let the automated testing environment verify and validate that you wrote the right code. Teams that have applied these approaches are often more productive and write code that has many fewer defects.
- Making non-fragile UI tests–Can you write acceptance tests for your application's GUI framework? If not, you are not writing code that can be tested automatically. Each operation that is available to users in your application should have an automated acceptance test.
- Dealing with legacy code–Legacy code is code for which unit or acceptance tests cannot be written, or code that must be refactored before it can have a good set of unit tests. Code that does not have unit testing is often just code that is poorly designed. The first step in dealing with legacy code is to refactor it so that unit tests can be written for it. Doing this will not only bring that code into your automated testing environment, but it will also improve the quality and design of the code itself. If you cannot refactor old code, then put it in some type of code quarantine so that it is isolated from any code against which unit testing is being run. This will help ensure that when a unit test fails, it is failing against a piece of code that can be actively fixed, maintained, or improved.
Automated testing exists for the primary purpose of helping to ensure that everything that your team is doing today does not break anything that your team did yesterday, the day before, and so on. Also, the tests that you write today will be the tests that you use going forward. And, the net result of having automated testing is the same as having incrementally developed regression testing.