TDD and Design By Contract - friend or foe? By Mario Gleichmann
In this column Ill try to answer a legitimate question: why do we need contracts (in the sense of invariants, pre- and post-conditions) when weve got unit tests right at hand that could also test the stated conditions?
In fact, it's very tempting to see unit tests as an all-embracing instrument to check and show that a class will behave correctly, making contracts unnecessary.
Ill try to show you that unit tests (as an instrument for test driven development) and contracts (as an instrument for Design By Contract) indeed share the same goals but aren't competing techniques, rather than methods that could complement each other.
Its not about verification - its about specification
First of all, we have to clarify a potential misconception: unit tests as an instrument in the sense of Test Driven Development (TDD) arent that much about verification of a correct implementation rather than about a specification of how a unit should behave. In fact, its the specification (not the verification) that will drive development. You can see the result of this core idea in the rise of Behaviour Driven Development (BDD) that mainly tries to find an adequate vocabulary to write down specifications (that of course can be verified automatically) in an easy natural way, refocusing on how a component should behave under certain conditions.
On the other side, Design by Contract is also about specifying the design, in terms of the behaviour of your components. A contract asserts truths about the design of your code in the form of runtime tests placed within the contracted units itself. These assertions will be checked as the thread of execution passes through the parts of those components, and will fire if they dont hold.
That said, both methods are motiviated by the same purpose - the specification of a system that will drive its design.
Don't Get Carried Away
As said, the goal of bdd is to refocus on the specification of a component. The other way around, it means that it might be easy to lose direction within tdd, risking to set focus on testing and verification. I have to admit that I walked into this trap more than once in the past. Of course the whole vocabulary (test, testcase, testsuite, etc.) makes you believe that its about testing. Its easy to become addicted to this idea, so even writing tests after implementation seems ok in this sense. Thats no longer possible if you see tests as specification: you firstly have to have a specification on what a component is intended to do - you cant start with an implementation until that specification exists.
Is the same true for Design by Contract? Is the contract metaphor misleading? Of course you could also define your contract afterwards, so again development of a component isn't driven by its specification. But the contract metaphor uses a more appropriate vocabulary. With invariants, pre- and post-conditions, its clearly about the specification of the intended behaviour - the risk of seeing a contract as a tool for verification might be much less.
Collaboration includes clients and supplier
Theres a special area where contracts are able to provide real benefits in regard to unit tests: oftentimes unit tests specify only the behaviour of a component as a supplier, that means the test asserts mostly the stated effects (the post-conditions of a method). Thats perfectly legal: most of the prospective clients may be unknown to the designer of the component or doesnt exist yet. Since the client is responsible for the adherence of preconditions it even should not be the in the job of the supplier.
A contract on the other hand also specifies the constraints under which its legal to call a method (the preconditions). Since contracts act on runtime, those preconditions will also be regarded when it comes to collaboration between an arbitrary client and the supplier component. The following points show some aspects of this holistic view:
Reliance on a proper context
Most developers (including me) tend to insert guard clauses in their code despite having unit tests. They dont seem to trust in the context in which the method gets called (thats of course a good idea if the context cant be foreseen). But youll even find this fact in environments where the same team controls the whole context. That may be also because of the before mentioned lack of knowledge about all clients that may call your component, especially in teams with limited communication. Another one might be the local (specification / test in a different file than the component) and the temporal (tests at design time - collaboration at runtime) 'distance' between the code and the test.
Leaving contracts aside, the code (as part of the public interface of a component) you can rely on at runtime, seems to increase reliablity in the running context. This may be because of...
Support of Defensive Programming With Design by Contract, contracts automatically protect methods from improper usage, like illegal arguments (in the simplest case) in the real hard world, checking potentially a wider range of collaborations than a test ever can do (tests cannot show the absence of defects, it can only show that defects are present). Like Bertrand Meyer said: a test checks one (or a limited set of) case(s). A contract describes the abstract specification for all cases.
That said, preconditions that will work at runtime, making guard clauses in a way redundant, because the method can rely that its called in a proper way. Thats a runtime feature you cant achieve with unit tests. They could only check or specify how a component should behave if its called in an improper way (for example by expecting to throw an exception, which becomes a post-condition). But this one wouldnt free you to write additional guard clauses.
Clear Reliable Documentation While tests can be seen as an additional artifact that specifies the expected behaviour of a component, contracts form part of the public (client-) view of a class - they are part of the public interface. Personally, Im lazy - I want to recognize the core behaviour of a building block by looking at its interface / signature, not studying an amount of code be it the building block itself or a test - but of course there are also other preferences that prefer a split between specification and implementation.
Of course those contracts are more than documentation, since assertions are checked at runtime, thereby testing that the stated contracts are consistent with what the routines actually do.
Integration 'tests' for free
This one follows directly from the initial point, when regarding collaboration between clients and suppliers: having contracts that can be turned on and off makes it very easy to test and retest parts of a program. It allows you for instance to continue to test class A while you are working on class B, in case there are any interactions you did not foresee. Of course, this is a kind of integration testing - its not about the specification of the behaviour of a single component but more about collaboration between a set of components and most of the time not in the scope of unit tests.
Clear Design
A contract's condition is an equivalent description of the algorithm used in a function, but written in an orthogonal manner, so thinking about such matters as pre-, post-conditions and invariants help to make the concept of a class more explicit. It might encourage you to think about the conceptual model of a building block. That might lead to a clear design: obligations and benefits are shared between client and supplier and are clearly stated.
The limitations on the use of a method are clearly expressed and the consequences of calling it illegally are clear, so programmers are encouraged not to build routines that are too general, but to design classes with small single-purpose routines. While TDD guides you more towards loosely coupled building blocks (because it hurts when trying to test a tightly coupled component in isolation), DBC guides you more towards smaller building blocks with a clear responsibility (because it hurts to write invariants of classes with more than one responsibility or write pre- and post-conditions for methods with more than one task).
'Debugging' Contracts can take part in real life, that means under real runtime conditions, maybe during development and user acceptance test - they take effect under real circumstances while real clients (no mocks) and real collaborators (maybe also contracted) interoperate with each other. So contracts also guarding the collaboration of multiple building blocks forcing to fulfill a collective adduced goal due to a specification. having clear sound messages that state the cause of a contract violation, helps locating where the fault lies semantically: its very easy to find the reason of a misbehaviour within a specific interaction, pointing directly to its origin (if a precondition is violated, than its the fault of the client, if an invariant or a post-condition is violated, than its the fault of the supplier).
Reuse
For library users (where tests may not be accessible), contracts clearly explain what library classes do and what constraints there are on using them. They provide feedback to someone learning to use other peoples classes: sometimes you only know the interface to operate with - the implementing class is unknown. Having contracts on that interface gives you a clear, sound concept of the limitations and relevant effects in programming against that interface in a proper way.
Open Close Principle Compliance
While tests usually test a single unit, DBC handles inheritance in a broader way, supporting also inheritance of contracts to subclasses and therefore forcing to adhere to the claimed behaviour, stated in contracts of a super class or interface. This goes hand in hand with the Liskov Substitution Principle.
Conclusion
As you might have seen - Design by Contract is contributing some alluring features and 'drivers' when it comes to the specification of the intended behaviour of a component or even a whole system of interacting objects. As seen, contracts also underline the importance of the proper collaboration between clients and suppliers - a feature thats mostly not in the scope of unit tests.
Those new drivers fit very nicely with the strengths of unit tests (not all mentioned here), making them a perfect fit for a 'specification driven development'. Until next time,
Mario Gleichmann
http://gleichmann.wordpress.com/ |