Foundations for Professionals .NET Professionals im Profil guide to C# guidgen.de

Blog

Nutze den Augenblick
und teile der Welt mit, was Du zu sagen hast.

Privaten Code testen

Samstag, 7. August 2010, 00:40 Uhr
Permalink | Kommentare (9) | Kommentare als RSSRSS

Vor einigen Tagen habe ich den Blogeintrag Wie viel kosten Unittests? geschrieben, um zu zeigen, dass der Einsatz von Unittests nicht aufwändiger und teurer ist als der Verzicht auf diese. Mein Fazit lautete:

Da ich – bevor ich die Entwicklung einer Komponente beginne – auf Grund von EBCs und TDD ohnehin detailliert plane, wie deren API aussehen und wie sich diese API für einen Verwender anfühlen wird, erfordert die Planung häufig deutlich mehr Zeit als die eigentliche Implementierung.

Der Aufwand, die Implementierung im Sinne von TDD noch mit Unittests auszustatten, geht daher gegen Null.

Auf die Idee für diesen Blogeintrag haben mich der Blogeintrag TDD in der (meiner) Praxis – Wunsch und Wirklichkeit von Thomas Bandt und der Thread Tatsächlicher Nutzen von Unit-Tests? auf myCSharp.de gebracht, die beide ebendiesen Aspekt in Frage gestellt haben.

In besagtem Thread auf myCSharp.de hat sich zwischenzeitlich noch eine weitere interessante Diskussion ergeben: Die aktuell dort diskutierte Frage lautet, inwiefern privater Code mit eigenen Unittests ausgestattet werden sollte.

Hierzu empfiehlt es sich, zunächst die verschiedenen Varianten zu evaluieren, wie dies überhaupt geschehen kann – schließlich ist der Zugriff auf privaten Code von außen per se zunächst nicht möglich.

  • Reflection: Die einfachste Variante, privaten Code von außen zugreifbar zu machen, liegt darin, per Reflection auf ihn zuzugreifen. Dies ist auch die Variante, die Visual Studio von Haus aus im Rahmen der automatischen generierten Zugriffsklassen für Tests bietet. Die Nachteile liegen auf der Hand: Da der Zugriff mit Hilfe von Reflection naturgemäß nicht typisiert erfolgen kann, muss auf Magic Strings zurückgegriffen werden – die spätestens bei umfangreichen Refaktorisierungen für Probleme und Unannehmlichkeiten sorgen.
  • Vererbung: Als zweite Möglichkeit bietet sich an, den Zugriffsmodifizierer private durch den Zugriffsmodifizierer protected zu ersetzen – auf diese Art wird privater Code zumindest für abgeleitete Klassen zugreifbar. Da prinzipiell nichts dagegen spricht, auch eine Testklasse von der zu testenden Klasse abzuleiten, kann der Zugriff auf diese Art hergestellt werden. Doch private und protected weisen eine unterschiedliche Semantik auf und sind daher nicht bedingungslos gegeneinander austauschbar. Zudem funktioniert dieser Trick nicht bei versiegelten Klassen, von denen nicht abgeleitet werden kann.
  • InternalsVisibleTo: Als weitere Möglichkeit kann der Zugriffsmodifizierer private auch durch den Zugriffsmodifizierer internal ersetzt werden. Wird zusätzlich das Attribut InternalsVisibleTo verwendet, fühlt sich der Zugriff auf privaten Code aus Unittests de facto so an wie der Zugriff auf nicht-privaten Code. Allerdings trifft der Nachteil der zweiten Variante hier ebenfalls zu – sogar in gravierenderem Ausmaß, da internal noch freigiebiger ist als protected.
  • Zustand als Abhängigkeit: Die vierte Variante schließlich wurde von Ralf Westphal in seinem sehr lesenswerten Blogeintrag Zustand als Abhängigkeit – IoC konsequent gedacht beschrieben. Diese Variante ist deutlich aufwändiger, packt das Problem jedoch an der Wurzel an und stellt – zumindest in meinen Augen – die beste der hier vorgestellten vier Varianten dar. Allerdings verfügt sie neben dem hohen Aufwand noch über einen weiteren Nachteil: Sie erfordert die Einführung eines zusätzlichen, nur für Tests gedachten Konstruktors – die Tests verwenden also nicht mehr den gleichen Konstruktor, der auch im Produktivcode zur Anwendung kommt.

Alles in allem ist das Testen von privatem Code also nicht einfach und wirft etliche Probleme auf. In TDD gilt die Regel, dass schwer testbarer Code in der Regel eine Schwäche in seiner Architektur aufweist. Nimmt man beides zusammen, folgt aus dem Bedürfnis, privaten Code testen zu wollen, dass die zu Grunde liegende Architektur des zu testenden Codes eine Schwachstelle aufweist.

Bernd Hengelein schreibt dazu in seinem Blogeintrag TDD – ein Garant für heiße Diskussionen:

Wenn man den Drang verspürt eine private Methode testen zu wollen, ist das ein Code Smell der auf eine Umstrukturierung der Klasse hindeutet.

Auf stackoverflow.com wird in How do you unit test private methods? ähnlich argumentiert:

If you want to unit test a private method, something may be wrong. Unit tests are (generally speaking) meant to test the interface of a class, meaning its public (and protected) methods.

Auch Wikipedia geht in dem Abschnitt Eigenschaften von Modultests auf ähnliche Art auf die Frage ein:

Modultests sollen gemäß dem Design by contract Prinzip möglichst nicht die Interna einer Methode testen, sondern nur ihre externen Auswirkungen (Rückgabewerte, Ausgaben, Zustandsänderungen, Zusicherungen). Werden auch interne Details der Methode geprüft (dies wird als White-Box-Testing bezeichnet), droht der Test fragil zu werden, er könnte auch fehlschlagen, obwohl sich die externen Auswirkungen nicht geändert haben. Daher wird in der Regel das sogenannte Black-Box-Testing empfohlen, bei dem man sich auf das Prüfen der externen Auswirkungen beschränkt.

Das Buch Pragmatic Unit Testing äußert sich ebenfalls ähnlich:

In general, we don’t want to break any encapsulation for the sake of testing (or as Mom used to say, “Don’t expose your privates!”).

Kurzum: Die Fachwelt ist sich einig, dass man privaten Code nicht testen sollte. Doch warum ist dies so? Wie liegt die architektonische Schwachstelle, deren Existenz man aus der bloßen Schwierigkeit, einen Test zu formulieren, ableiten kann?

Die Schwachstelle liegt im Kontrakt der zu testenden Klasse. Jeder Code weist einen Kontrakt auf – implizit oder explizit als dedizierte Schnittstelle – der für die Verwender dieses Codes gedacht ist: Auf das, was mir öffentlich zugesichert wird, kann ich mich verlassen. Dies gilt dabei nicht nur im Hinblick auf die Syntax, sondern explizit auch im Hinblick auf die Semantik.

Alles, was nicht im Kontrakt enthalten ist, wird mir als Verwender auch nicht explizit zugesichert. Anders formuliert: Alles, was nicht im Kontrakt enthalten, sondern private ist, zählt zu den Interna – die mich als Verwender nichts angehen. Das ist das Prinzip einer Blackbox.

Was ist nun der Sinn eines Unittests? Der Unittest soll dem Entwickler einer Klasse garantieren, dass er diese derart refaktorisieren kann, dass er ihre innere Struktur ändert, ohne zugleich den nach außen zugesicherten Kontrakt zu ändern. Ein Unittest ermöglicht dem Entwickler, das Innere der Blackbox nach Belieben zu verändern, sofern ihre Funktionsweise nach außen erhalten bleibt.

Würde man nun beginnen, auch das innere der Blackbox mit Unittests abzusichern, so würde man dem Entwickler diese Freiheit nehmen und in seinem Revier wildern. Eine interne, durchaus sinnvolle, aber ausgesprochen große Refaktorisierung würde hiermit unmöglich gemacht – schließlich könnten potenziell alle Unittests, die auf das Innere der Blackbox abzielen, fehlschlagen.

Die Unittests müssten also angepasst werden – was aber der ursprünglichen Absicht widerspricht, mit den Unittests sicherzustellen, dass das Verhalten vor und nach der Refaktorisierung identisch ist. Kurzum – das Testen von privatem Code ist nicht nur umständlich, sondern geht auch noch an der eigentlichen Absicht der Tests vorbei.

Die einzig verbleibende Frage lautet nun noch, wie mit privatem Code umgegangen werden sollte – wenn er sich schon als ungeeignet für Unittests erweist?

Die Antwort auf diese Frage ist einfach: Zunächst einmal gilt, dass auch privater Code getestet wird – nur eben indirekt. Ruft eine öffentliche Methode privaten Code auf, so wird dieser implizit ebenfalls getestet – ist dies nicht der Fall, handelt es sich eventuell um Code, der gar nicht benötigt wird, oder um einen Hinweis darauf, dass noch nicht alle potenziellen Fälle per Test überprüft werden.

Für private Methoden gibt es jedoch noch einen weiteren Ansatz, wie mit ihnen umgegangen werden kann: Häufig enthalten private Methoden nämlich Code, der gar nichts mit dem eigentlichen Zweck der sie enthaltenden Klasse gemeinsam hat – streng genommen handelt es sich also sogar um eine Verletzung des Single Resposibility-Prinzips (SRP).

Die Lösung hierfür lautet: Auslagern des privaten Codes in eine eigene Klasse mit dedizierter Verantwortlichkeit. Diese weitere Einführung zusätzlicher Klassen oder Komponenten mit eigener Verantwortlichkeit führt damit nicht nur zu besserer Testbarkeit, sondern automatisch auch zu einer Verbesserung der Architektur – und kann in nahezu allen Fällen dieser Art angewandt werden.

Am Anfang mag sich dies ein wenig ungewohnt anfühlen – doch wenn man darüber nachdenkt, findet man beinahe in jedem Fall entsprechende Verantwortlichkeiten, die vermischt wurden. Diese aufzulösen, ist dann ein Leichtes.

Abschließend kann noch festgestellt werden, dass das Problem des Testens von privatem Code hauptsächlich Entwickler betrifft, die nicht nach TDD vorgehen. Dies ist jedoch nicht verwunderlich, denn TDD-affine Entwickler schreiben zuerst den Test und dann erst den Code: Privater Code kann hier also nur durch Refaktorisieren von bereits bestehendem und getestetem Code entstehen.

TDD sorgt also auch in diesem Fall für eine bessere Architektur – für mich persönlich, ein weiterer Grund, den mit TDD eingeschlagenen Weg weiter zu verfolgen.

Wie viel kosten Unittests?

Dienstag, 3. August 2010, 10:03 Uhr
Permalink | Kommentare (3) | Kommentare als RSSRSS

Vor rund zwei Wochen hat Thomas Bandt in seinem Blogeintrag TDD in der (meiner) Praxis – Wunsch und Wirklichkeit die Frage aufgeworfen, wie sehr Theorie und Praxis bei TDD auseinanderklaffen. Prinzipiell gesteht er TDD zunächst einen gewissen Charme zu:

Test Driven Development, wozu ich insbesondere „test first“ zähle, hat für mich einen gewissen Charme. Es ist zweifelsfrei so, dass dadurch die Architektur der eigenen Anwendung verbessert wird und viele kleine Fehler vermieden werden können.

TDD betrachtet er also prinzipiell – rein akademisch gesehen – als erstrebenswert. Doch dann äußert er seine Zweifel:

Die Frage ist nur: zu welchem Preis?

Er ist jedoch nicht der einzige, der sich mit dieser Frage beschäftigt: Hagen Siegel hat auf myCSharp.de den Thread Tatsächlicher Nutzen von Unit-Tests? begonnen. Die für ihn essenzielle Frage lautet:

Ich selbst habe Zeifel am Nutzen von Unit-Tests, wenn man den Aufand ins Verhältnis setzt. Führt der sachgemäße Einsatz von Unit-Tests tatsächlich zu qualitativ hochwertiger Software?

So wohl Thomas wie auch Hagen führen also den Preis beziehungsweise den Aufwand als Argument an und fragen sich, ob das Verhältnis zwischen diesem und dem Nutzen im richtigen Verhältnis steht.

Dieser Frage begegnet man in der Diskussion mit Entwicklern, die TDD noch nicht oder noch nicht durchgängig einsetzen, immer wieder. Reduziert auf ihre kürzeste Form lautet sie:

Wie viel kosten Unittests?

Häufig wird als Antwort impliziert, dass Unittests im Allgemeinen und TDD im Speziellen zu teuer seien, um es durchgängig und konsequent anzuwenden. Mich erinnert das an meine Argumentation vom vergangenen November, als auch ich mich gefragt habe: Wie viel Sinn machen Unittests?. Mein Fazit damals lautete:

Die Frage ist also nicht, ob eine 100%ige Testabdeckung wünschenswert ist – dies gilt rein von der Theorie her ohne jegliche Zweifel – sondern ob eine 100%ige Testabdeckung mit vernünftigem und vertretbaren Aufwand erreicht werden kann: Diese Frage kann durchaus mit Nein beantwortet werden.

Wie stehe ich heute zu dieser Antwort?

Für mein heutiges Empfinden sind all diese Fragen falsch gestellt. Die grundlegende Frage nach den Kosten sollte nämlich nicht

Wie viel kosten Unittests?

lauten, sondern vielmehr:

Wie viel kostet es, auf Unittests zu verzichten?

Ich will diese Frage an Hand eines aktuellen Beispiels beantworten. Derzeit arbeite ich an einem EBC-basierten Prototypen einer bestehenden Anwendung. Primär dient dieser Prototyp dazu, die Fähigkeiten von EBCs auszuloten – für mich ist es jedoch zusätzlich das erste Projekt, in dem ich konsequent und strikt nach 4-Step TDD vorgehe.

Gestern Abend habe ich bemerkt, dass ein Kontrakt nicht so entworfen war, wie er hätte sein sollen: Weder hat die Vererbungshierarchie noch hat die Terminologie gepasst. Da ich die Pfadfinderregel der Clean Code Developer-Initiative

Hinterlasse einen Ort immer in einem besseren Zustand als du ihn vorgefunden hast.

sehr schätze, habe ich beschlossen, den Kontrakt zu refaktorisieren. Da dieser Kontrakt eine sehr grundlegende Rolle in der Anwendung spielt und daher in vielen Projekten der Solution genutzt wird, hatte meine Refaktorisierung Auswirkungen auf nahezu die Hälfte der Projekte.

Nach rund 90 Minuten war meine Refaktorisierung so weit gediehen, dass der Compiler die Anwendung wieder übersetzen konnte. Früher, das heißt, in den Zeiten, in denen ich noch kein TDD angewendet habe, hätte ich die Anwendung nun vermutlich gestartet, um die wichtigsten Funktionen sicherheitshalber noch einmal zu testen: Funktioniert alles noch so, wie ursprünglich vorgesehen?

Dass Fehler bei einem derartigen Test potenziell unentdeckt bleiben, liegt auf der Hand – es ist schlicht und ergreifend nicht möglich, alle denkbaren Fälle durchzugehen. Dies ist weder zeitlich noch logistisch zu meistern, erst recht nicht nach jeder Änderung, Erweiterung oder Refaktorisierung.

Daher hätte ich wohl oder übel darauf hoffen müssen, dass die Anwendung noch so funktioniert wie vorher.

Vielleicht hätte sie das getan – gestern jedoch tat sie es nicht.

Die Ausführung aller Tests dauert derzeit knapp 45 Sekunden. Neun davon sind gestern Abend fehlgeschlagen. Ich wusste also nach weniger als 45 Sekunden, dass meine Anwendung nicht mehr so funktionierte wie vorher. Die Fehler zu finden, war leicht. Die Fehler zu beheben, war ebenfalls leicht. Nach rund 10 Minuten liefen alle Tests wieder erfolgreich durch.

Was bedeutet das nun?

Das Refaktorisieren hätte auch ohne Tests 45 Minuten gedauert, denn die Tests waren von der Refaktorisierung nicht betroffen. Doch ich wage zu bezweifeln, dass mich das Entdecken, Erkennen, Finden und Beheben der Fehler nur 10 Minuten gekostet hätte, wenn ich die Anwendung ohne Tests per Hand hätte überprüfen müssen – wenn ich die Fehler dann überhaupt entdeckt hätte.

Natürlich ist nicht garantiert, dass die Tests jeden denkbaren Fall abdecken – doch auf das, was getestet wird, kann ich mich verlassen. Jederzeit.

Was verbleibt, ist die zum ursprünglichen Schreiben der Tests benötigte Zeit. Diese fällt ausgesprochen gering aus, da ich jede EBC isoliert für sich testen kann und daher keine Zeit in aufwändiges Mocking stecken muss. In Welchen Nutzen bieten EBCs? habe ich diesen Vorteil zumindest implizit bereits thematisiert:

Damit gewinnt man Topologieunabhängigkeit, denn jede EBC ist von ihrer Umgebung entkoppelt und kann entweder für sich alleine oder transparent im Verbund mit anderen Komponenten genutzt werden. Dass sich die Funktionalität von EBCs damit perfekt per Unittest validieren lässt, liegt auf der Hand. Daher eignen sich EBCs ausgezeichnet auch für die Entwicklung mit 4-Step TDD.

Da ich – bevor ich die Entwicklung einer Komponente beginne – auf Grund von EBCs und TDD ohnehin detailliert plane, wie deren API aussehen und wie sich diese API für einen Verwender anfühlen wird, erfordert die Planung häufig deutlich mehr Zeit als die eigentliche Implementierung.

Der Aufwand, die Implementierung im Sinne von TDD noch mit Unittests auszustatten, geht daher gegen Null.

Anmut und Eleganz

Samstag, 1. Mai 2010, 09:37 Uhr
Permalink | Kommentare (9) | Kommentare als RSSRSS

Am 13. Oktober 2008 haben Peter Bucher und ich unter dem Titel Noch Fragen, Bucher? Ja, Roden! angekündigt, jeweils zum ersten eines jeden Monats einen Kommentar zu einem vorab gemeinsam gewählten Thema verfassen zu wollen. Bisher sind in dieser Reihe folgende Kommentare erschienen:

Heute, am 1. Mai 2010, ist es nun wieder so weit, und unser Thema für diesen Monat lautet:

Anmut und Eleganz

So wohl Peter wie auch ich haben uns unabhängig voneinander im Vorfeld unsere Gedanken gemacht, wie wir diesem Thema gegenüberstehen. Peters Kommentar findet sich zeitgleich in seinem Blog, folgend nun mein Kommentar zu diesem Thema:

Anmut und Eleganz – zwei Begriffe der Ästhetik und der Schönheit. Laut Wikipedia wird Anmut als

Form des Schönen, […] der unwillkürliche Ausdruck einer Harmonie

definiert, wohingegen Eleganz als

Ausdruck von besonderem Stil und Geschmack in Design, Architektur, […]

gilt und nicht nur ein ästhetisches Konzept, sondern gar ein entsprechendes Ideal darstellt.

Intuitiv werden diese Begriffe nicht zwingend mit Code assoziiert, doch spielen sie auch in der Entwicklung eine entscheidende Rolle: Je eher Code eine gewisse Anmut und Eleganz ausstrahlt, desto eher folgt seine innere Form wohlüberlegten Gedanken.

In anderen Worten: Code, an dessen innerer Form gefeilt wurde, ist nicht nur wart- und evolvierbarer als über die Zeit emergierter Code – er folgt zudem einer eigenen Ästhetik: Der des im mathematischen Sinne Eleganten.

Jeder Entwickler, der in seiner Ausbildung mit Mathematik in Berührung gekommen ist, kennt den Term einer schönen Beweisführung. Der Term schön bedeutet in diesem Kontext, dass der Beweis elegant geführt und kompakt ist wie auch auf überflüssige Haken und Wendungen verzichtet wird.

Dies klingt zunächst nach dem Konzept der minimalistischen Architektur: Ein Entwurf ist dann gelungen, wenn kein weiterer Bestandteil aus dem Gesamtbild entfernt werden kann, ohne dass die Qualität oder die Funktionalität des Entwurfs darunter leidet.

Insofern liegt die Vermutung nahe, dass anmutiger und eleganter Code gleichzusetzen sei mit minimalistischem Code: Dass Code so lange reduziert, Ausdrücke vereinfacht und zusammengefasst, und aufwändige Konstrukte durch kompakte ersetzt werden sollten, bis eine minimalistische Form erreicht ist, die ohne Qualitäts- oder funktionale Einbußen nicht weiter reduziert werden kann.

Gilt also die Gleichung anmutiger und eleganter Code gleich minimaler, kompakter und reduzierter Code?

Während dieses Ziel in der Mathematik durchaus in dieser Form gelten könnte, lässt es einen wesentlichen Aspekt von Code aus: Im Gegensatz zu einem mathematischen Beweis, der – einmal erfolgt – nur noch zu Referenzzwecken nachvollzogen wird, wird Code immer und immer wieder gelesen.

Die gängige Wendung

It was hard to write, so it should be hard to read.

ironisiert diesen Aspekt. Fakt ist, dass Code im Idealfall nur ein Mal geschrieben, aber potenziell unzählige Male gelesen wird – nicht nur von anderen Entwicklern, auch vom ursprünglichen Autor: In der Regel gilt dies spätestens dann, wenn Code auf Grund neuer oder geänderter Anforderungen entweder erweitert oder zumindest angepasst werden muss.

Code sollte also für den Leser leicht verständlich sein. Hierzu trägt zunächst die äußere, optische Form von Code einen essenziellen Teil bei. Entsprechende Fragen lauten beispielsweise:

  • Tragen Bezeichner verständliche und beschreibende Namen, die semantisch zum Inhalt passen?
  • Erläutern Kommentare, aus welchen Gründen der kommentierte Code auf diese Art und nicht anders entwickelt wurde?
  • Hielt der Entwickler formale Kriterien wie Verwendung von Elementen wie Einrückung und Leerzeilen ein?
  • Folgen so wohl Code wie auch Kommentare und Dokumentation der gültigen Rechtschreibung und Grammatik?

Doch neben dieser äußeren Form spielt auch die innere Form für die Verständlichkeit eine bedeutende Rolle. Die entscheidende Frage hierzu lautet, ob dem jeweiligen Zweck angemessene Sprachkonstrukte angewandt wurden. Ausgewählte Beispiele dafür bieten folgende Aspekte:

  • Werden LINQ-Abfragen an Stelle von aufwändigen Schleifen mit zahlreichen, unter Umständen verschachtelten if-Anweisungen genutzt?
  • Wie werden Schnittstellen, abstrakte Basisklasse und konkrete Klassen miteinander kombiniert?
  • Werden Konstrukte wie beispielsweise das yield-Schlüsselwort dort eingesetzt, wo sie sinnvoll sind und Arbeit ersparen können?

Um diese Sprachkonstrukte angemessen einsetzen zu können, ist es erforderlich, sein Werkzeug zu beherrschen – sprich, die dargebotenen Mittel der persönlich gewählten Programmiersprache nicht nur zu kennen, sondern sie zu beherrschen.

Zur Verbesserung der inneren Form gibt es zahlreiche Regeln – bereits Werkzeuge zur statischen Codeanalyse wie FxCop oder die in Visual Studio integrierte Codeanalyse können hierbei eine wesentliche Hilfestellung leisten.

Je länger man sich jedoch als Entwickler mit diesen Themen beschäftigt und versucht, während der Entwicklung bewusst so wohl auf die innere wie auch auf die äußere Form von Code zu achten, desto eher stellt sich – auf Basis der nach und nach verinnerlichten Regeln – ein Gespür für ebenjene Anmut und Eleganz ein, die wart- und evolvierbaren Code auszeichnet.

Da dieses Gespür für ästhetisch gelungenen Code von handfesten Regeln getragen wird, besteht keine Gefahr, Initiativen wie beispielsweise Clean Code Developer zuwider zu handeln: Im Gegenteil – da dem Gespür für Ästhetik und Eleganz die Internisierung der Regeln zu Grunde liegt, erleichtert es letztlich den Umgang mit ebendiesen.

Eine Anmerkung in eigener Sache: Dieser Beitrag ist der – inzwischen achtzehnte – Kommentar im Rahmen von Noch Fragen, Bucher? Ja, Roden!. Unsere Reihe geht nun vorerst in eine Kreativpause, in der wir am Konzept und neuen Themen feilen werden. Auf absehbare Zeit werden wir diese Reihe fortsetzen.