Test Driven Design by Contract

 
19 mei 2009

Laatst hoorde ik iemand beweren dat in statisch getypeerde talen Design by Contract (DbC) een superieur alternatief is voor Test Driven Design (TDD). Mijn standpunt was andersom: TDD is veel meer dan alleen unit testen en zelfs unit tests zijn niet volledig vervangbaar door DbC. Ik heb de belangrijkste argumenten in de discussie nog eens bij elkaar gebracht in deze post. Het is zeker geen volledig en uitgediept artikel, maar ik hoop dat het toch genoeg aanknopingspunten bevat om op te reageren. Ik ben benieuwd wat anderen hiervan denken.

Ik zie DbC meer als een alternatief voor documentatie. Waar een ontwikkelaar normaal gesproken pre- en postcondities in de documentatie dient aan te geven, kan hij dat met DbC technieken direct op een formele manier beschrijven, te controleren door de compiler. Best cool! Maar, wat ik tot nu toe gezien heb van bijvoorbeeld Spec#, maakt dat ik me afvraag of het niet misschien nog wat te vroeg is om de techniek in C# te gebruiken. Het verbruikte best veel processor capaciteit. Maar goed, misschien zijn andere implementaties sneller. En ik heb nog niet gehoord van snelheidsproblemen bij het gebruik van DbC in Erlang.

Goed, tijd om mijn zaak ter verdediging van TDD te openen. Ik zal een aantal PRO’s voor het gebruik van TDD als techniek eerst behandelen en dan de PRO’s van DbC, allebei waar toepasselijk, vergeleken met de ander. Praktische bezwaren zoals snelheidsproblemen laat ik er buiten, die lijken me oplosbaar.

PROs TDD

  1. TDD zorgt ervoor dat je je eerst focust op de interface van wat je aan het 0ntwikkelen bent. Dit helpt een ontwikkelaar om een elegante interface te bedenken dat weinig overhead oplevert voor de client code. Zoiets zie ik niet gebeuren bij het gebruik van DbC.
  2. Unit tests testen niet alleen het contract van een method, maar ook zijn implementatie. Dit lijkt me wel belangrijk.
  3. Geautomatiseerde unit tests geven je een vrijwel intantaan zicht op de problemen in je code, net als DbC je dat geeft d.m.v. compiler errors (of vergelijkbaar).

PROs DbC

  1. DbC helpt een ontwikkelaar continue bij het gebruik van een API. Unit tests moet je eerst hebben en je moet er ook in kijken, voordat ze je helpen bij het gebruik van een API (ik ga dat niet vertalen met consumeren).
  2. DbC helpt een ontwikkelaar om minder defensief te programmeren. Argument controle en Excepties worden vervangen door pre- en postcondities, die compile time gecontroleerd worden.
  3. DbC helpt je om pre- en postcondities in een consistente en nuttige manier vast te leggen. Als je ervan uitgaat dat je ze toch moet schrijven, dan kost dat dus geen extra tijd. Het schrijven van unit tests is een investering van ontwikkeltijd die je niet heel direct terugverdient. [Dit argument vind ik nogal discutabel, omdat een grote hoeveelheid API documentatie geen pre- en postcondities bevat. Zou dus wel eens niet zo nuttig kunnen zijn als DbC enthousiastelingen denken en kost bovendien dus wel extra tijd. En het schrijven van tests is een bewezen manier om de kwaliteit van je code te verhogen en daarmee dus tijd te winnen doordat minder debugging noodzakelijk is]

Zoals je kunt zien blijven de positieve effecten van TDD en DbC overeind in vergelijking met elkaar. Dus ik denk dat TDD and DbC geen vervanging van elkaar zijn. Er is enige overlap. Je zou misschien een aantal unit tests kunnen laten vallen die je zonder DbC zou schrijven om de afhandeling te testen van argumenten die buiten de normale grenzen gaan. Maar over het algemeen zie ik ze nu vooral als elkaar aanvullend.

DbC helpt om een methode vollediger te beschrijven dan een naam ooit kan doen en op zo’n manier dat er compile time gechecked kan worden op fouten. Maar volgens mij schrijf je documentatie pas als het ontwerp van je code begint te stabiliseren. Voor code die nog veel verandert, is documentatie alleen maar een vertragende factor bij het refactoren. TDD focust je aandacht op het schrijven van code die goed te gebruiken is, een API die goed te gebruiken is en levert je meteen unit tests op. Natuurlijk leveren unit tests ook wel wat vertraging op bij het refactoren, maar leveren je daarbij echter ook heel veel tijd op doordat je weet dat je code nog werkt. Bovendien focus je meer op de bedoeling van een methode of class, wat het minste zou moeten veranderen. Als je goede unit tests schrijft.

Tenslotte ben ik niet de enige die denkt dat TDD en DbC elkaar aanvullen: Het artikel over TDD op Wikipedia vermeldt hetzelfde. En hier is een artikel dat hetzelfde beschrijft, waarin zelfs een nieuwe stijl van ontwikkelen daarmee in het leven wordt geroepen, genaamd Agile Specification-Driven Development.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development

Tags: , , , ,


Reacties (9)

  • @Mendelt

    Nou, eerste reactie was: “Tis alleen een andere manier van opschrijven.” – dat komt toch wel heel dicht in de buurt van gelijk en vervangbaar. Alleen op een andere manier opgeschreven. :-)

    En ik snap wel dat je TDD en DbC wilt vergelijken, maar alle beweringen die je deed over TDD hebben betrekking op het hebben en schrijven van unit tests. TDD is meer, in mijn ogen.

    Over het met beide voeten op de grond blijven ben ik het helemaal met je eens. Zelf gebruik ik nu TDD met bij voorkeur, maar 100% project coverage haal ik niet (eerder 75% ofzo). Maar ja, welke practice wel? En inderdaad, op vele plekken is proven code correctness nog steeds een nieuw begrip. En al helemaal niet in de praktijk gebracht… er is nog veel te doen!

    Geplaatst op 05 juni 2009 om 11:58 Permalink

  • Hoi Rick!

    Volgens mij heeft er nog niemand gezegd dat TDD en DbC gelijk en vervangbaar zijn. Ze hebben hetzelfde doel maar verschillende “blinde vlekken” dus ze vullen elkaar in veel gevallen goed aan.

    Ik vind het juist interessant om DbC met TDD te vergelijken en niet met unit tests :-) De kracht van TDD is juist de dynamiek in je project. Dit is precies een van de blinde vlekken van DbC.

    Bovendien TDD is design, DbC is design, unit testen is testen.

    Maar voordat wij als twee archicecture astronauts in een baan om de aarde verdwijnen is de laatste zin uit Andre’s verhaal natuurlijk een stuk pragmatischer…

    Geplaatst op 29 mei 2009 om 9:13 Permalink

  • Leuk om te zien dat er zoveel discussie loskomt. Wellicht goed om mijn niet-genuanceerde stelling wat toe te lichten.

    In mijn optiek zijn tests en contractspecificaties twee vormen van checking of je code correct werkt – en zal werken. In feite is het in deze lijn ook ongeveer hetzelfde als een compiler en eigenlijk de algemene werkzaamheden die een programmeur uitvoert – het omzetten van bepaalde requirements in een stukje executeerbare code.

    Waarom scheer ik nu deze twee verificatie / specificatiemethoden over één kam? Ik denk niet dat het loont om hier zo gigantisch veel effort in te steken. Dat is in het verleden veel gedaan (bewijzen dat je code werkt stamt al van lang geleden) maar in de praktijk is het lastig en tijdrovend. Daarnaast is het bedrijven van DbC / TDD bij complexe (wellicht slecht opgezette) code ook niet eenvoudig te noemen.
    Ik denk dat het veel meer loont en dus veel interessanter is om je druk te maken hoe je je code zelf minder complex maakt. Momenteel is de beste manier om dat te doen in mijn optiek het goed opzetten van een model. Dit is ook niet simpel maar past wel beter in in een top-down software engineering benadering dan beide low-level technieken die hier vergeleken worden.

    Meer effort steken in modelleren en als gevolg hiervan het produceren van betere modellen betekent eenvoudigere en lossere koppeling en dus minder noodzaak voor testen of contractspecificatie. Die plekken die overblijven zijn uiteraard belangrijk om goed over na te denken maar welk framework of techniekje je daarvoor kiest vind ik niet interessant: als je er maar goed over nadenkt. Vandaar, DbC == TDD: Gebruik er nou in elk geval voor de interessante stukken die na goed ontwerp overblijven gewoon eens ééntje, dat vind ik al heel wat :).

    Geplaatst op 28 mei 2009 om 18:46 Permalink

  • @ Mendelt

    Ik ben het met je eens dat ze (deels) dezelfde bedoeling hebben. Maar om dan te zeggen dat ze gelijk (en vervangbaar zijn). Een boot en een auto gebruik je ook allebei om jezelf mee te vervoeren, maar ze zijn niet voor elkaar te substitueren.

    Verder ben ik het grotendeels wel eens met wat je schrijft, als je het woord TDD zou vervangen door ‘unit tests’. Want dat is mijns inziens zeker niet hetzelfde..

    Geplaatst op 28 mei 2009 om 12:22 Permalink

  • Volgens mij is de opmerking TDD == DbC niet helemaal uit de lucht gegrepen. De bedoeling van beide ideeen is vrijwel hetzelfde. Je schrijft ontwerpbeslissingen op een manier op die door een computer te controleren is. Er is ook nogal wat overlap maar in de praktijk zijn de twee concepten wel verschillend. Vooral omdat ze beide wat beperkingen hebben. Omdat ze andere beperkingen hebben vullen ze elkaar in veel gevallen wel mooi aan.

    TDD test alleen post-condities. Je test het resultaat van het aanroepen van een stuk code, het is nog steeds mogelijk dat andere code (die dan blijkbaar minder goed getest is) jouw code aanroept op een manier die niet aan de pre-condities voldoet, invariants zijn ook niet te checken. (DbC zonder static analysis heeft hier ook nog wat moeite mee)

    DbC is vaak beperkt in expressiviteit, vaak kunnen alleen waarden van variabelen op een simpele manier gechecked worden. Als pre of postcondities complex worden zijn ze vaak lastig leesbaar. TDD, zeker samen met mocking frameworks is hier vaak beter in.

    Unit tests kun je op ieder moment uitvoeren. DbC kan met behulp van static analysis ook op bijvoorbeeld een build server tot test resultaten leiden maar dit vangt niet alle fouten af. Pre en postcondities worden pas volledig getest bij het uitvoeren van de code.

    Geplaatst op 27 mei 2009 om 12:32 Permalink

  • @ André als je zegt TDD == DbC dan verwacht ik eigenlijk wel dat het wat meer is dan dat ze in de kern hetzelfde zijn, dat ten eerste. :-)

    Maar dat terzijde, dan ben ik het nog niet helemaal met je eens. Om mijn beweringen nu ook eens in expressies te vatten: TDD > unit tests en unit tests != DbC, maar {unit tests, dbc} implements InterfaceSpecification.

    Anders gezegd: TDD werkt vanuit de aanroepende code om zowel de interface als de aangeroepen code (implementatie) van daaruit te laten volgen. DbC werkt vanuit de bestaande, uitgespecificeerde interfaces en laat je van daaruit een nieuw stuk implementatie toevoegen met weer een nieuwe interface daarbovenop.

    Dus volgens mij is, naast dat TDD en DbC andere methodes gebruiken om interfaces goed uit te specificeren, het ook zo dat TDD werkt vanuit het doel (aanroeper) naar de mogelijkheden (implementatie) en DbC juist andersom.

    Ik ben het dus eens met Daan. :-) Een relevante quote van Knuth ook in dit verband: “Beware of bugs in the above code; I have only proved it correct, not tried it.”

    Leuk trouwens Jasper, dat jij het ook hebt over DbC dat verder gaat dan TDD. Dat is denk ik waar (in de formalisatie van interfaces), maar TDD gaat óók verder dan DbC door zich ook te richten op het ontwerp van een eenvoudig bruikbare interface/code en een implementatie die doet wat de interface belooft.

    Geplaatst op 27 mei 2009 om 11:58 Permalink

  • Daan Wanrooy schreef:

    Ik ben het eens met Rick. Ook in mijn ogen zijn TDD en DbC niet aan elkaar gelijk. Wel bestaat er enige overlap.

    Wanneer ik methodes schrijf gooi ik excepties wanneer er foutieve invoer is. Dit mechanisme wil ik zelf ook testen. DbC zou het testen van dit mechanisme overbodig maken.

    Geplaatst op 27 mei 2009 om 10:34 Permalink

  • Mijn ‘mening’: TDD == DbC. Tis alleen een andere manier van opschrijven. DbC is niks anders dan formeel specificeren, wat we jaren terug al in bijvoorbeeld Z deden. TDD is specificatie van je contract maar dan met ‘gewone’ code en randgevallen, vaak iets wolliger van aard.

    Maar in de kern zijn ze hetzelfde.

    Geplaatst op 21 mei 2009 om 15:18 Permalink

    • Jasper Stein schreef:

      @André:
      Dat lijkt me nogal een boude bewering! Volgens mij is er geen eenduidige definitie van DbC, zodat het maar net de vraag is wat je daaronder verstaat. Hoort een Debug.Assert()-aanroep daar ook al bij? Dan kan je in ‘gewoon’ Java en C# dus al DbC doen. Het lijkt me dat TDD dan wel iets toevoegt.

      Bij DbC hangt het er dus maar net vanaf hoever je wil (en kan) gaan; dwz. hoe sterk of zwak je taal is waarin je de contracten opschrijft, en op welk nivo die contracten worden gecontroleerd (bijv. runtime of compile-time). Code Contracts gaat al wat verder als ik de handleiding mag geloven (geen persoonlijke ervaring mee, helaas), net als Spec# en Eiffel.

      Als je naar functionele talen gaat kijken dan kan je daar al echt richting de formele verificatie gaan. De taal Clean heeft een specifieke op de taal gerichte stellingbewijzer, en in een systeem (programmeertaal is het niet echt) als Coq leven de programma’s, specificaties, contracten en bewijzen alle op hetzelfde nivo.

      Door de kracht van het typesysteem van Coq kan je binnen het systeem zelf je contract specificeren als wiskundige propositie, en er vervolgens ook binnen het systeem een bewijsvoering over leveren, die dus ook weer door de compiler gecheckt kan worden. Kortgezegd: contract = propositie, en programma dat hoort bij het contract = bewijs v/d propositie.

      Dat gaat dus een aantal stappen verder dan TDD, waar het bijv. altijd nog maar de vraag is of je niet net nog een randgeval over het hoofd hebt gezien. En mijn (beperkte) ervaring met unit tests schrijven is dat het soms ook de nodige moeite kost om die unit tests zelf bugvrij te krijgen.

      Geplaatst op 26 mei 2009 om 22:41 Permalink