Changing code is a big challenge for ensuring software quality and software design as well. It’s an important but often underestimated topic. If new features should be implemented or existing requirements change you should be able to change your code in a way that ensures correctness for the behavior of the new and the existing code base and you should be able to keep a consistent design.
With TDD you set up a nice test suite and tests can be run in a reproducible way. If requirements change or are extended and you adapt/extend your code, with TDD you first have to rewrite and/or extend your tests to fit the new set of requirements. But then by running your adapted test suite you can catch unexpected side effects from your code changes. This regression testing is a great safety net for those changes. Without such a test suite how could you be sure that the expected behavior still holds after the code has changed? Tests in a TDD way are great for continuous integration. When a developer in your team changes code and performs a check-in, with clear unit tests you are able to validate the changes during a continuous integration build. With a constraint on the minimum value of allowed code coverage this gives you a great confidence that a code change has no side effects which affect the expected behavior of your components. Moreover TDD ensures a more consistent API design over code changes. By writing tests before implementing new requirements you continuously adapt your design from a client view.
A drawback of tests is the effort to manually write and adapt tests. When behavior changes and old tests fail you have to investigate why those tests fail, thus what the old behavior has been and if this behavior has changed or if your implementation is simply wrong. With huge test suites maintainability can become a very time-consumptive task. Another important issue with TDD and automated tests in general is that any developer who joins your project must have a good comprehension for the TDD process and for automated tests. For many developers TDD is not very intuitive at the beginning and it’s overwhelming them by completely exchanging their development styles and habits. Becoming familiar with TDD could be an important gap for developers and giving all developers in a project the same comprehension for the process can be difficult (especially if they don’t see the benefits of TDD). That’s a question of accountability and project lead. There have to be project guidelines which include testing standards and there must be mechanisms to watch the adherence of those standards.
DbC can’t give you huge support for code changes out-of-the-box. If you only rely on runtime checking of the contracts you could not make a statement if an implementation still holds the contractual specification when code changes. Moreover you have to manually adapt your contracts when you change an implementation. There’s no direct mechanism besides the limited static checking that ensures the correctness of your implementations in terms of the defined contracts. The only solution is to set tests in place that validate the contracts for exemplaric input/output pairs. With such a test suite you can check in a reproducible way if your implementation still mirrors your contracts. But note: I don’t say that tests for contracts should be written manually. In my opinion it doesn’t make much sense, because both tests and contracts are a form of specification and with manually testing a contract you would duplicate the specification code! Moreover contracts already contain the relevant information which is needed to set up a test automatically. Solutions like Pex can jump in here to generate tests from scratch or from parameterized unit tests, using contracts as test oracles for accepted input values (preconditions) and expected output (postconditions).
Contracts have a gap for developers as well. Contracting can get messy and confusing if you are not able to give the developers on your project the very same comprehension of DbC: how and where to use contracts, limits for contract definitions and so on. As further drawback there is no possibility to check the percentage of contracting in a fashion of code coverage in TDD. A developer with no commonsense for the process could leave contracts and nobody would recognize and react. Again it’s the responsibility of the project lead to give guidelines on contracting and to observe the adherence of those guidelines.
Clean code principles
TDD and DbC both give great support to some important clean code principles and thus improve the software quality far beyond the bug prevention aspect.
- YAGNI (You Ain’t Gonna Need It):
DbC cannot support you here, but the TDD process greatly supports YAGNI. For each new feature you write a test which leads to an implementation that reflects the test suite and which contains only those features that are really necessary.
- SRP/SoC (Single Responsibility Principle/Separation of Concerns):
Both DbC and TDD support those principles. TDD has less impact, but in my opinion with TDD you early discover the dependencies of your components and thus you are encouraged to keep components clean. DbC is more offensive in terms of SRP and SoC. It’s difficult to write contracts for components with many responsibilities and thus DbC directly enforces a separation of concerns.
- KISS (Keep It Simple and Stupid):
Both TDD and DbC add some value here. With TDD due to the incremental evolution an API is kept simple and client-friendly. For DbC the same rules apply as for SRP/SoC: It’s easy to define simple and stupid methods with clear contracts, but it’s hard to define contracts on complex and messy components.
- DRY (Don’t Repeat Yourself):
I don’t see support from TDD, but DbC influences the DRY principle. Because DbC introduces contracts that clarify benefits and obligations in client/supplier communication it prevents clients from checking return values and parameters redundantly. For example a postcondition says “the return value will never be null”. Thus there is no need for a client of the method to check the return value before handing it to another method that expects a non-null value as argument. This would be encouraged when you do defensive programming, but with DbC you can really prevent such redundancies.
- OCP (Open/Closed Principle):
I don’t see big influence from both DbC and TDD here. OCP states: “Software entities should be open for extension, but closed for modification”. This means that your components should be easily extensible without the need for code modifications. OCP increases the level of abstraction and the code complexity, but it makes components easily extensible. With TDD you drive your API only for a current feature set and there seems to be no impact on extensibility. DbC with its contracts can’t help you here as well in my opinion.
- ISP (Interface Segregation Principle):
Both DbC and TDD support the ISP. DbC influences ISP by the enforcement of SRP/SoC. When you set contracts on an interface with many responsibilities/members it would be painful to write invariants that must be maintained by each interface member. But it’s pretty easy if you have clear and simple interfaces. For TDD the impact is less, but it’s there since you early discover dependencies and you drive your API in a “segregated” direction when implementing tests for new features.
- DIP (Dependency Inversion Principle):
Support for this principle mainly comes from TDD, but DbC can add little value as well. With TDD to test a component in isolation you have to exchange dependencies of this component with test doubles like mocks or stubs. Thus you need abstractions/interfaces for those dependencies which enforces DIP. Then with Dependency Injection you are able to inject a test double at runtime of a test. The influence of DbC is more subtle. DbC encourages the use of interfaces at whole by enforcing uniform behavior over all implementations of an interface. Thus the use of interfaces instead of concrete implementations in terms of DIP is encouraged as well.
- LSP (Liskov Substitution Principle):
TDD can’t help you here, but DbC adds great value. The LSP states that in class hierarchies it must be possible to treat a derived object as if it would be an object of the base class. Thus the specialized object must behave in the same way as the base class object. This principle of clear sub-classing is a basic part of DbC. Inheritors of a base class or implementors of an interface are not allowed to add new preconditions, but they can extend postconditions and invariants with additional contracts. Thus DbC guarantees uniform behavior in terms of the LSP.
- CQS (Command-Query Separation):
TDD doesn’t encourage you to do CQS. But with DbC you need to separate commands as pure methods and queries in order to use commands in contracts. Thus DbC leads to command-query separation which is a good thing since it keeps your API clean (SoC) and your components simple (KISS).
This and the last blog posts gave a comparison of DbC and TDD by taking several aspects into account: specification, design, documentation, code coupling, universality, expressiveness, correctness checking, code changes and influence on clean code principles. While writing this little series of blog posts things became much clearer to me. When starting with DbC some months ago I just thought: “Well, forget TDD, DbC with static checking is all what we need and what we should use“. But that was thought too short. TDD goes far beyond testing and writing unit tests. At its heart TDD is more about design, documentation and specification and it’s really valuable in those (and other) terms.
Both DbC and TDD have advantages and drawbacks and both act on their own terrains with interesting overlaps. And it’s absolutely not a mutual exclusive choice between both principles. However TDD and DbC should be used in conjunction to utilize the advantages of both principles. In fact there should be a clear TDD process that makes use of contracts. As the previous blog posts showed DbC can support TDD, but there’s no possibility to replace it in any way.