The benefits of automated testing are well understood, and widely accepted as a good practice in software development in 2010. Unfortunately, in cases where we need automated testing the most, it is difficult to introduce. The best example is a large code base that is poorly constructed but important to our organization. Michael Feathers has written an excellent book Working Effectively with Legacy Code which describes the problem and how to tackle it in the real world. His book is a tremendous resource for people who wish to introduce automated testing to rotten code that lacks it.
In addition to the excellent ideas in that book, I believe code contracts can also help you achieve higher quality and greater testability. In this article I will explain how this is possible.
First we need to understand the problem. What do I mean by poor quality code? And why is it difficult to introduce automated testing for a poorly written code base? By definition, such code is unreliable, inflexible, unreadable, and unsupportable. It lacks comprehensive automated test to ensure correctness and support refactoring or feature changes. Such code may have long and complex methods. It may have classes that are not cohesive. The overall class design has excessive coupling and lacks dependency injection. All coding is done against concrete classes and there is no evidence of interfaces anywhere. There are other undesirable qualities but that is enough for now. Nobody wants to change the code for any reason, because the risk of causing regression is so high, and the cost of detecting that regression through manual testing is so high.
Wow! It sounds like a mess doesn’t it? I wish we could say these kinds of systems were rare. Unfortunately, there are probably several systems with this poor level of design and implementation that are being coded even as this is written. You might even be working on one of them, in an environment that does not support you in your efforts to follow good practices.
Why do these qualities make it difficult to write automated unit tests? Michael Feathers explains this in Chapter 3. Sensing and Separation from Working Effectively with Legacy Code:
“Dependencies among classes can make it very difficult to get particular clusters of objects under test. We might want to create an object of one class and ask it questions, but to create it, we need objects of another class, and those objects need objects of another class, and so on. Eventually you wind up with the nearly the whole system in a harness.”
In the rest of his book, he explains how to undertake the changes needed to put the entire application under test. I will not repeat that. Instead, I will explain how the use of code contracts can help us to detect regression and increase our confidence to make changes.
Code contracts provide assertions that sometimes resemble the assertions that would be present in an automated test. While poor code quality makes it difficult to introduce automated tests, such problems with code structure seem to have much less of an effect on our ability to introduce code contracts. In other words, it is fairly easy to introduce code contracts into a code base regardless of whether it is well structured or a mess. This means that code contracts are much easier to introduce in an incremental fashion. Furthermore, contract programming can improve the quality of our code regardless of whether automated testing is present.
Keep in mind that even though code contracts add value on their own, they are not a full replacement for the power of automated tests. In an ideal world, if I had to support a large and important software product and it did not have code contracts or automated unit tests, I would establish a long-range goal that includes both
- establishing a comprehensive suite of automated tests
- incorporating a comprehensive collection of code contracts into the code
So I’m not recommending or encouraging people to abandon the important but difficult job of introducing automated tests in support of their large software products. For those who wish to follow the example encouraged by Michael Feathers, and with Legacy code under test, code contracts will accelerate you along that journey. For those who do not wish to invest the effort to put Legacy code under test, code contracts are an economical means to enhance the quality in some modest measure.
The challenge with poor quality code is that it needs all kinds of improvements, which can only happen if you can change the code. You cannot change the code because you don’t have the safety net of an automated test suite that provides full scenario-relevant coverage to detect regression. But you cannot build automated tests because it is not testable. You cannot refactor it to make it more testable because you cannot safely change it. If you enjoy this conversation, you can repeat this chain of events in a circle until you are exhausted.
This is where code contracts can come to the rescue. Code contracts are easily introduced on an incremental basis, wherever and whenever they are needed, regardless of the quality of the code in which they are inserted. When a programmer is following proper contract programming practices, the introduction of code contracts should introduce no measurable change in the logical flow of the software. Proper contracts are free of side effects (visible changes). Because the contracts do not change the logical flow of the software, you can introduce them into the code base with a low risk of introducing regression. This is the main reason they are easy to introduce in an incremental way.
In a similar way, automated unit tests can be introduced to a code base without causing regression from the test itself. The logical flow of the program is not altered by the creation of unit test. That is not where the problem lies. The challenge for introducing automated testing into a problematic code base is due to the fact that you may have to re-factor the code in order to make it testable by automation. It is this refactoring that introduces risk and inhibits developers from pursuing the development of automated unit tests for a messy code base.
Another useful feature of Microsoft code contracts is the ability to customize how the contract runtime checking behaves on contract failure. In a healthy code base, I would normally throw exceptions whenever a code contracts failed. In such a code base, defects are rare, and I want to catch them and change the code to repair the defect immediately. But in an extremely unhealthy code base, there may be so many defects, that throwing exceptions when they arise would make the software completely unusable and untestable. In this case, you could modify the contract failure behavior to detect and report defects, without throwing exceptions. You could later modify this behavior to throw exceptions once the code base as a more manageable level of defects.
In a poor quality code base, which lacks comprehensive automated tests, you still have to rely on manual testing until you can build a sufficiently rich library of automated tests. In traditional manual tests, you set up the initial conditions, perform some action, and compare the actual results to the expected results. This can be very time-consuming and labor-intensive. Such manual testing may be good for validating the final output. But in a poor quality code base, if defects are detected, it can be very time-consuming for programmers to identify where the defect is located. You know it is somewhere between the start of the manual test and the end. But there may be quite a bit of code in between . Code contracts provide greater leverage for manual testing in the following way. If the code is covered with a liberal sprinkling of contracts, then a single end-to-end will trigger all kinds of contract assertions. Each of these assertions is equivalent to built-in test verification. If any defects are detected, they will be reported as exceptions which are easy to observe. Such contract failures provide a clear and quick path for programmers to isolate the faulty portion of the code so that it can be more easily corrected. And if the contracts are used properly, they will detect problems as early as possible in the execution process.
In summary, we have discussed how code contracts by themselves enhance the quality of our software. They also assist us if we are interested in placing our legacy code under test. In either case, whether we attempt the short-term investment of just adding contracts to improve our ability to detect defects, or whether we pursue a more ambitious goal of placing the legacy code under automated tests, code contracts are appealing because they are easy to apply selectively and incrementally where and when they are needed.
By deliberately introducing code contracts into legacy code, we can improve the quality at a low, incremental cost which is easily incorporated into the work cycle of even the busiest programmer. By introducing these gradual improvements, it will make our lives easier and the experience of our customers more satisfying.