Vor knapp einer Woche stellte Stefan die Frage „Wohin mit den Tests?“ gestellt. Er selbst schreibt, er trenne die Implementierung und deren Tests strikt, ließ sich allerdings zu einem Experiment hinreißen, in dem er Implementierung und Tests in einem einzigen Projekt ließ. Alles in einer Assembly also. Motiviert wurde er wohl durch meine Aussage, dass es schlichtweg einfach nicht mehr zeitgemäß ist, Tests vom SUT zu trennen.

Und genau so ist es auch. Zumindest für mich. Ich habe lange Zeit Tests und Implementierung in separaten Projekten entwickelt. Bis ich bemerkt habe, dass es mir nicht hilft, sondern mich eher bei meiner Entwicklung stört. Seitdem halte ich Tests und Implementierung immer ganz nah beieinander, sozusagen „Seite an Seite“. Ich möchte in diesem Post meine Beweggründe & Konsequenzen für Side-by-Side-Specs (oder Near-Specs, wie ich es gerne nenne) schildern, und somit meine Antwort auf Stefan’s Frage geben.

Nun zunächst einmal beschränke ich mit bei den Near-Specs auf Unit Tests. Ganz klassisch im Sinne des TDD. Integrationstests sind bei mir nach wie vor Far-Specs, also separate Projekte. Doch es soll ja um die Unit Tests gehen, besser gesagt um Tests, die aus dem Test-First-Ansatz heraus entstehen. Das können Tests bei TDD oder aber auch Specs bei BDD sein. Ergo: Tests, mit denen ich meinen Code designe & entwickle, halte ich sehr nah am Code.

Verhalten greifbar formen

Doch wieso sind bei mir die Tests nun an der Implementierung dran? Es gibt einen ganz einfachen und für mich auch unschlagbar effektiven Grund: Die Tests sind greifbar. Sie sind dort, wo ich sie brauche, nämlich an dem Verhalten, für das ich sie zur Spezifikation gebraucht habe. Seit dem ich die Tests an der Implementierung habe, „forme“ ich auch mehr und öfter. Soll heissen: Ich führe öfter und effizienter Refactorings durch. Nicht nur vom SUT, sondern natürlich auch von den Tests selbst.

Überhaupt ist es mit den Tests im Implementierungsprojekt ein ganz anderes, viel effizienteres arbeiten. Wenn ich nach einiger Zeit wieder einmal in einem Projekt etwas zu tun habe, schaue ich mir die Tests an. Das Schöne bei den Near-Specs ist jetzt, dass ich sehr schnell zwischen Spezifikations- und Implementierungssicht wechsle und deutlich zügiger navigieren kann. Das fällt mir besonders bei Tests mit mehreren Abhängigkeiten / Mocks auf. Ich komme mit dieser Art von „Code lesen“ viel besser zurecht als vorher mit dem hin- und herspringen zwischen mehreren Projekten.

Zugegeben, anfangs tat ich mir noch ein wenig schwer, denn schließlich ist die Anzahl der Testklassen – genereller: die Menge an Testcode – für ein SUT meist deutlich mehr. Neben der Umgewöhnung des „nicht mehr über Assembly-Boundaries“ hinweg arbeitens hatte ich das Gefühl, dass mich der Testcode „stört“ oder mir „im Weg steht“. Doch dieses Gefühl war schnell wieder weg, als ich das Prinzip auf größere Projekte angewendet habe und immer wieder die Tests für die Spezifikation und das Codeverständnis gebraucht habe. Spätestens dann war bei mir die Erkenntnis da: Side-By-Side-Specifications Rule!

Doch mit dieser „Colocation“ der Tests an die Implementierung tauchen neue Fragen auf. Eine ganz besonders oft gestellte Frage ist die der Auslieferung der Tests in Release-Assemblies. Es ist schließlich erstrebenswert, nur das in ein Release einfliessen zu lassen, was auch wirklich vom Benutzer gebraucht wird: die Funktionalität. So stand ich auch vor dieser Aufgabe, welches sich aber sehr schnell und vor allem einfach bewältigen ließ.

Reduce to the Max

Die Idee ist denkbar einfach: Wenn ich Tests und Implementierung zur Entwicklungszeit nicht trenne, dann trenne ich sie eben später, undzwar zur Auslieferungszeit. In einfachen Worten: der Build muss diese Aufgabe erledigen. Da ich sowieso meinen Code über Build-Skripte bauen lasse, lag es sehr nah, diese Skripte einfach anzupassen. Für meine eigenen Projekte habe ich oft ein kleines Powershell-Skript. Bei größeren Projekten oder CI-Umgebungen ist der Aufruf eines eigenen Post-Clean-Events vor dem Build ein guter Extension-Point.

Als ich die Menge und den Inhalt der Kommentare bei Stefan’s Post gesehen habe, dachte ich mir nur: warum schreiben so viele darüber und finden es „schwierig“ oder „unsauber“, ohne es offensichtlich einmal probiert zu haben? Ich habe es probiert und ich fand es überhaupt nicht schwierig – ganz im Gegenteil. Jetzt ist das für mich eine viel angenehmere, effizientere und vor allem intuitivere Arbeitsweise. Das Build-Skript zum filtern der Testartefakte ist denkbar einfach, bei mir sind das meistens nur so 10 bis 20 Zeilen. Schließlich ist es ja auch „nur“ ein Release-Build. Ich rufe es nicht die ganze Zeit auf wie ein CI-Build, und CD mache bzw. brauche ich derzeit auch nicht.

„Soooo lange, wie ich brauche, diese Kommentare überhaupt zu lesen, brauche ich nicht einmal, wenn ich sogar das Build-Skript per TDD erstelle“ war mein Gedanke, als ich mit dem Lesen der Kommentare fertig war. Und da ich ein Freund von empirischen Erkenntnissen bin, dachte ich mir: „Auf geht’s, das machst Du jetzt!“. Gesagt getan. Das Ergebnis kann man sich auf github ansehen. Erkenntnis: Die Entwicklung des Build-Skripts (ohne TDD Framework und mit meinen „bescheidenen“ Powershell-Kenntnissen) hat etwas mehr als eine Stunde gedauert. So lange habe ich in etwa auch zum Lesen der Kommentare gebraucht ;-)

Separation of Concerns

Doch zurück zum Thema. Beim Lesen der Kommentare ist mir auch aufgefallen, dass man öfter das Argument der „Separation of Concerns“ (SoC) gegen die Zusammenführung von Tests und Implementierung anbringt. Es sei im Sinne der SoC, Tests von der Implementierung zu trennen. Auch gute Freunde von mir, wie z.B. Tehlike – für den ich im Übrigen diesen Post auch auf Englisch verfasst habe – entgegneten mir das gleiche Argument. Dazu mein Statement:

Tehlike, yanılıyorsun! ;-)
(Auf deutsch: Tehlike, Du irrst Dich!)

Ja, ich denke diejenigen irren sich, die SoC als Argument für eine Trennung von Tests & Implementierung anbringen. Sie irren sich meiner Meinung nach sogar gewaltig. Denn SoC ist in meinen Augen bei genauerer Betrachtung sogar ein Argument für die Zusammenlegung von Tests und Implementierung.

Separation of Concerns besagt nämlich, dass unterschiedliche Zuständigkeiten eines Objekts voneinander getrennt werden sollten. Zuständigkeiten sind im Sinne der Domänenmodellierung und Systemanalyse Aspekte von Verhalten und Funktionalität. Ergo: Unterschiedliche Zuständigkeiten eines Verhaltens sollten voneinander getrennt werden. Ein schönes Beispiel ist der Taschenrechner. Die Funktionalität des Addierens des Taschenrechners gestaltet sich aus dem Tastenfeld, der Recheneinheit, und der Anzeige. All das sind Aspekte der Funktionalität, die in eigenständige Zuständigkeiten münden. Im Sinne der „Separation of Concerns“ sind diese Aspekte zu trennen und damit die Komponentenorientierung und Wiederverwendbarkeit zu stärken.

Completeness of Concern

So weit, so gut. Doch Separation of Concerns bedeutet für mich im Umkehrschluß auch, dass man eine Zuständigkeit einer Funktionalität auch zusammenhalten sollte. Ein einziger „Concern“ sollte in sich geschlossen sein. „Das ist er doch automatisch“ denkt man sich. Naja, ganz so einfach ist es meiner Meinung nach nicht. Denn eine Zuständigkeit einer Funktionalität findet man in der Software-Entwicklung oftmals nicht als „ganze Einheit“ an einem Fleck, sondern überall verteilt.

So wird z.B. in der klassischen Softwareentwicklung die Zuständigkeit in den Requirements erfasst. Man findet sie in der Use-Case-Dokumentation und in UML-Diagrammen wieder, diesmal mit ein oder zwei Szenarien. Natürlich wird das Verhalten und dessen Aspekt auch in den Tests manifestiert – dazu sind sie ja da. Und – wer hätte das gedacht – die Funktionalität findet man auch im Code wieder. Resultat: Ein einziger Concern, der im Entwicklungsprozess an allen möglichen Stellen auftaucht. In einem solchen Szenario ist es für den Entwickler meistens wie ein Puzzle, bei dem die Puzzlestücke überall sind – nur nicht auf dem Tisch.

Es ist also für die „Vollständigkeit einer Zuständigkeit“ nicht hilfreich, Tests und Implementierung voneinander zu trennen. Weiterhin hat das Trennen von Tests und Implementierung nichts mit Separation of Concerns zu tun, denn schließlich geht es sowohl bei den Tests als auch bei der Implementierung um einen einzigen Concern – dem Aspekt der Funktionalität, die das SUT eben abbilden soll.

Es ist gerade die Fraktion der BDD-Anhänger, die genau diesem Umstand mit all Ihren Argumentationen und Prinzipien hervorheben möchten. Sie reden nicht von Tests, sondern von Spezifikationen, sie reden nicht über Verifikation, sondern über Verhalten. Ja, sie gehen sogar soweit und reden im Sinne der Context-Specifications von „Concerns“, also „Zuständigkeiten“. Das alles sind klare Hinweise, das die Spezifikation eines Verhaltens und deren Umsetzung als unzertrennliches Paar zu betrachten sind. Übrigens ist diese „Expressivität“ auch ein Grund, warum „BDD“ so gut geeignet ist für Einsteiger in TDD bzw. Test-First.

Feels just like it should

Wenn man mal eine gewisse Zeit per TDD/BDD entwickelt, dann fällt einem oft auf, dass man bei jeder Änderung mindestens zwei Dinge ändert. Man ändert oder erweitert die Tests und man ändert oder erweitert die Implementierung. Ein Verhalten, eine Zuständigkeit, doch zwei Perspektiven, zwei Änderungspunkte.

Das ist bei Far-Specs, (also getrennten Tests und Implementierung über Assembly-Boundaries) genauso wie auch bei Near-Specs (Tests & Implementierung Seite-An-Seite). Doch bei Near-Specs ist es einfacher, schneller und intuitiver, Änderungen am System durchzuführen. Darüber hinaus wird das Streben nach „Vollständigkeit eines Verhaltens und deren Zuständigkeit“ gestärkt. Das wiederum zahlt auf Lesbarkeit, Wartbarkeit und Flexibilität ein.

Seit nun seit knapp 3 Monaten entwickle ich Unit Tests, TDD & BDD ausschließlich per „Side-By-Side-Specifications. Diese „andere“ Art der Codeorganisation ist für mich äußerst angenehm und effektiv gleichermaßen. Für mich ist es ein stückweit näher dran am „TDD-Spirit“, dem test- bzw. spezifikationsgetriebenen designen und entwickeln von Software. Mit Tests & Implementierung nebeneinander fühlt es sich so an, wie es eigentlich sein sollte.

Also: Feels just like it should ;-)

Comments
This article has 5 comments:

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

  • @Mike: Also mit...
    by Ilker Cetinkaya
    on July 12th 2010

    @Mike: Also mit den Artefakten gehe ich relativ einfach um. Eine Namenskonvention (bzw. zwei) helfen mir, praktisch alle selbstgeschriebenen Tests herauszufiltern. Das gilt für Tests wie auch für Mocks oder Samples. Für die Referenzen habe ich ein Dictionary – ich verwende praktisch nur eine handvoll Tools. NUnit, XUnit, Moq, Rhino.Mocks. In die “Verlegenheit” des öfteren anpassens des Scripts bin ich bis dato nicht gekommen. Meist war nach 1-2 Edits Ruhe. Zu der “Test-Infrastruktur”-Änderung kann ich nur sagen, dass ich nicht alles Near-Spec verwalte. End-2-End und Integrationstests sind bei mir nach wie vor Far-Specs. Insofern ändert sich da bei mir an der “Infrastruktur” wenig. Ab und an wechsle ich die Frameworks. Aber wie gesagt, das hält sich sehr in Grenzen.

  • Hi Ilker, das hört...
    by Mike Bild
    on July 12th 2010

    Hi Ilker,
    das hört sich, auf den ersten Blick, wirklich produktiver an. Ist schon etwas nervig, Test und Implementierung zu trennen sowie die Struktur nachzuhalten. Bevor ich meine lang antrainierte Vorgehensweise einer Projekttrennung nun aufgebe, hab ich noch ein paar Fragen.
    Mal abgesehen von den eher Werte- und Regelgetriebenen Argumenten wie SoC und SRP – Wie gehst Du mit Mock-Klassen, Regressionstests mit Excel-Sheets oder ähnliches, verschiedene Konfigurationsfiles wie StructureMap-Konfigurationen usw. um? Passt Du bei Änderungen/Erweiterung der Test-Infrastruktur jedes Mal deine Build-Scripte an? Wirfst Du entsprechende, erfasst über z.B. Namenskonventionen, aus deinen Release-Build-Scripten raus? Ich hab da lieber ein Projekt das nicht Teil des Release-Build-Prozesses ist. Das Release-Build-Script bleibt möglichst gleich. Nur weil ich was an der Test-Infrastruktur ändere, möchte ich nicht gleich noch einen Task für eine Release-Infrastrukturänderung erzeugen. Der Deploy-Admin freut sich – womöglich muss er täglich Änderungen am Nightly-Build vornehmen. Da sich womöglich ausschließlich der Testprozess (TDD, BDD) und nicht der Gesamtprozess vereinfacht, bin ich mal misstrauisch was die Teamproduktivität also die Gesamtproduktivität betrifft.
    Anders herum wäre es schöner – das Build-System erzeugt Struktur-, Klassen- und Projekt-Templates (Maven lässt grüßen) und ich bin gezwungen, eine im Projekt notwendige Struktur einzuhalten. Damit steigt das Verständnis (durch automatische Regeln) für alle und die Produktivität würde sich auch verbessern. Leider kenn ich keine einfache Lösung für .NET? Hier jemand?

  • Hi Ilker, ich finde...
    by Thorsten Hans
    on July 12th 2010

    Hi Ilker,

    ich finde den Ansatz den du fährst gut, schneller Zugriff auf die Tests, noch schlankere Projektstrukturen. Was will man mehr.

    Stefan hat in seinem Post die Files noch schön gruppiert mit vsCommands, was ich auch sehr interessant finde.

    Zu der Build Thematik kann ich nur sagen, dass die TestFiles mit einer Zeile MSBuild Script entfernt werden können, sodass diese nicht in die finale Assembly kommen. Mehr als einfach.

    **Thumbs Up**

    Super Idee.

  • Hallo Ilker, beim Lesen...
    by Stefan Lieser
    on July 12th 2010

    Hallo Ilker,

    beim Lesen deines Posts ist mir klar geworden, dass auch ich Near-Specs habe. In einer Solution liegen bei mir immer nur zwei Projekte. Ferner enthält das Implementierungsprojekt meist nur eine handvoll Dateien. Dadurch sind bei dieser Form der Entwicklung, mit sogenannten Komponentenwerkbänken, die Tests ebenfalls ganz nah am Code.

    Zu SoC würde mich intereessieren, wie du SoC von SRP abgrenzt. Mir scheint nämlich, dass wir diese Begriffe unterschiedlich verwenden. Für mich hat eine Komponente eine Verantwortlichkeit (Responsibility). Die Verantwortlichkeit besteht aus mehreren Aspekten (Concerns). Einer der Aspekte ist die Implementierung, ein anderer die Tests (auf hoher Flughöhe betrachtet).

    Ein weiterer Punkt der mir nicht klar ist: wieviele Projekte sind typischerweise in deinen Solutions? Wie viele Dateien (Klassen) sind typischerweise in deinen Projekten? Davon hängt meiner Ansicht nach maßgeblich ab, wann man die Tests nahe genug an der Implementierung empfindet.

    Herzliche Grüße
    Stefan

  • [...] To Whom...
    by .NET Stories: Digitale Erfahrungen » Blog Archive » To Whom It May Concern ?!?
    on July 12th 2010

    [...] To Whom It May Concern ?!? 12 July 2010 Keine Kommentare Note: Diesen Artikel gibt es auch auf Deutsch [...]


(c) 2000-2012 ilker.de - Creative Computing.

For any case of inquiry regarding this document, you can always contact the website owner.