Let’s come to another blog post in preparation of elaborating the synergy of DbC and TDD. My first blog post on this topic covered an initial discussion on specification of code elements. Thereby it has shown different characteristics of DbC and TDD in terms of code specification. Since specification is one really important aspect in comparison of DbC and TDD, it’s of course not the only one. Hence today’s blog post is a starting point for a more general comparison of DbC and TDD with some other important aspects. There are 3 more blog posts that I will come up with during the next weeks, forming a 4-part comparison series.
To say first, Test-Driven Development (TDD) and Design by Contract (DbC) have very similar aims. Both concepts want to improve software quality by providing a set of conceptual building blocks that allow you to prevent bugs before making them. Both have impact on the design or development process of software elements. But both have their own characteristics and are different in achieving the purpose of better software. It’s necessary to understand commonalities and differences in order to take advantage of both principles in conjunction.
The last blog post has already given a dedicated look at the specification aspect and how DbC and TDD can add some value. To summarize both principles extend the code-based specification in their own ways. TDD let’s you specify the expected behavior of a code element in an example-driven and reproducible way. It’s easy to use and allows the expression of any expected behavior. DbC on the other side sets universal contracts in place that extend the definition of a code element and are tightly coupled to it. It’s a great concept for narrowing the definition of a code element by defining additional physical constraints as preconditions, postconditions and invariants. By defining contracts on interfaces DbC strengthens the role of interfaces and enforces identical constraints/behavior over all implementations of an interface. However not every behavior can be expressed by contracts and they’re bound to a single code element. Thus they don’t lessen the position of tests, but can be seen as great complement.
Both DbC and TDD have impact on the design of an API. Well, there is a slight but important difference as the names imply: it’s Design by Contract, but Test-Driven Development. Thus with DbC (contract-first) on the one side you are encouraged to write your contracts as you design your components (at design phase), which perfectly fits with the idea of contracts as extension of a component’s definition. With TDD and the test-first principle on the other side you write a test which maps to a new feature and afterwards you directly implement the code to get the test to green state. Hence TDD is tightly coupled to the development phase in contrast to DbC, which seems to come first. In addition personally I wouldn’t fight a religious war on this naming. If you think of DbC as „Contract-First Development“ or „Development by Contract“ you would have the contract-first principle coupled to the development phase as well. The more important thing is to find a way to effectively use contracts in the development cycle. If you are an advocate of up-front design you would perhaps want to set your contracts at design phase. But if you intensively use TDD it would be difficult to go down this design phase road. However you would set your contracts at development phase in conjunction with the test-first principle. This leads to the question of an effective development model with TDD and DbC and that’s another important story…
But for now let’s come back to the impact of DbC and TDD to the design of an API. With TDD you write a new test for each new feature and then you bring this test to green by implementing some piece of logic. This is some form of Client-Driven Development. Your test is the client of your API and you call your methods from the client’s perspective (as a client would do). If the current API doesn’t fit your needs, you extend or modify it. Thus the resulting API is very focussed on the client’s needs and furthermore doesn’t contain code for unnecessary features, which is a great thing in terms of YAGNI. Another impact of TDD is that it leads to loosely coupled components. Tests in form of unit tests are very distinct and should be coupled to the tested component, but not far beyond that (other components that are called, e.g. data access). Thus there is a certain demand for loose coupling e.g. by DI.
With DbC and contracts on your components you have other impacts. Contracts clarify the definition and intent of your components. When you come up with contracts you strengthen your opinion about a component by setting contractual obligations and benefits. This leads to much cleaner components. Moreover your components will have fewer responsibilities (and thus a better cohesion) since it would be painful to write contracts for components with many different responsibilities. Thus DbC is great in terms of supporting the SRP and SoC. Another impact comes from the „limitation“ of contracts to support only pure methods as part of a contract. If you want to use class methods in contracts (e.g. invariants) of this class you have to keep those methods pure. This leads to the enforcement of command-query separation by contracts, which very often is a good thing in terms of comprehensibility and maintainability.