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.

Ein Ordnungssystem für Unittests

Sonntag, 22. November 2009, 07:06 Uhr
Permalink | Kommentare (2) | Kommentare als RSSRSS

Auf den Tag drei Wochen sind vergangen, seit Ralf Westphal mich in Reaktion auf den Blogeintrag Wie viel Sinn machen Unittests? ermuntert hat, mich nochmals intensiv mit diesem Thema auseinanderzusetzen:

Unit Tests […] konsequent und kompetent mal für einen Monat einzusetzen. Mit der Kompetenz mag es da schwierig sein, weil man sich dann eine Veränderung der Handlungs- und Denkgewohnheit selbst beibringen muss... aber das ist zumindest ein Anfang.

Ich bin seinem Rat gefolgt und habe bemerkt, dass die wesentlichen Probleme für mich nicht technischer, sondern gedanklicher Natur waren. Zu viele unbeantwortete Fragen standen im Raum – Fragen, die zunächst einfach erscheinen, deren Beantwortung aber viel Nachdenken voraussetzt.

Man könnte mein Problem als die Herausforderung bezeichnen, ein tragfähiges Konzept für Unittests zu finden, das sich nahtlos in meine bisherige Arbeitsweise integriert, sie zwar erweitert, aber nicht von Grund auf umkrempelt.

Bemerkenswert fand ich, dass die wenigsten Entwickler, die ich mit diesen Fragen konfrontiert habe, ihre Antworten fundiert begründen konnten. Einige haben sich sogar daran gestört, dass ich diese Fragen überhaupt stelle, und haben eher dazu geraten, einfach loszulegen – frei nach dem Motto: TDD ist keine Wissenschaft!

Doch ohne fundierte Antworten auf grundlegende Fragen zu haben, die den eigenen Wissensdurst stillen, verbleibt immer ein ungutes Gefühl – ob das, was man macht, tragfähig für zukünftige Erweiterungen sein wird. Diese zukünftige Tragfähigkeit bedeutet mir sehr viel – anders wäre ich vermutlich auch von Extreme Programming nicht so begeistert.

Außerdem ist die Beantwortung solcher Fragen entscheidend, um klare Regeln und Richtlinien zu haben, auf deren Basis man Entscheidungen treffen kann: Je genauer diese Regeln und Richtlinien formuliert werden, desto größer wird die persönliche Liebe zum Detail, desto besser wird die Accuracy – meines Erachtens eine essenzielle Eigenschaft eines guten Enwicklers.

Die erste dieser Fragen, ob Code zum Zwecke einer besseren Testbarkeit angepasst werden darf, wurde bereits in Auch Kirchen haben Kragsteine diskutiert: Wie so oft ist es Ralf Westphal gelungen, eine anschauliche Analogie zur realen Welt zu finden, die das digitale Problem schlagartig löst.

Die zweite dieser Fragen, wie Unittests sinnvoll und übersichtlich organisiert werden können, wurde inzwischen auch beantwortet: Auch hierzu hat Ralf Westphal einen wesentlichen Teil beigetragen, aber auch Neno Loje, Bernd Marquardt und Peter Bucher haben mein nun gefundenes System beeinflusst.

Die grundlegende Erkenntnis ist zunächst, dass sich eine Testklasse nicht zwingend auf eine zu testende Klasse beziehen muss – dass also nicht zwingend eine 1:1-Beziehung zwischen beiden vorliegt. Statt dessen bezieht sich eine Testklasse auf ein sogenanntes System under test (SUT). Ein solches SUT kann durchaus eine Klasse sein – genausogut kann es aber auch eine einzelne Methode sein.

Die zweite Erkenntnis ist, dass ein Unittest ein SUT immer in einem gegebenen Kontext testet. Bei einer Methode ist das fast immer der Aufruf derselben, bei einer Klasse hingegen kann es verschiedene Ausgangssituationen geben: Ein Kontext wäre das Initialisieren der Klasse, ein weiterer die Arbeit mit der gerade initialisierten Klasse, wieder ein weiterer das Disposen einer bereits verwendeten Klasse.

Da sich in der Regel mehrere Unittests auf einen solchen Kontext beziehen, macht es durchaus Sinn, diese in einer Testklasse zusammenzufassen, deren Name den Kontext beschreibt, und deren Namespace den Platz des SUTs angibt.

Auf diese Art könnten Unittests, die den Aufruf der Methode ToDictionary in der Klasse goloroden.de.Common.ExtensionMethods testen, beispielsweise in der Testklasse

  • goloroden.de.Tests.Common.ExtensionMethods.WhenToDictionaryIsCalled

abgelegt werden. Die einzelnen Unittests beschreiben dann mit ihrem Namen innerhalb dieser Klasse nur noch die übergebenen Parameter sowie das erwartete Ergebnis:

  • WithNull_AnArgumentNullExceptionIsThrown()
  • WithAnEmptyString_AnArgumentExceptionIsThrown()

Der Vorteil dieser Terminologie liegt klar auf der Hand: Schlägt ein Unittest fehl, lassen sich aus Testklasse und –methode auf einen Blick alle relevanten Informationen ablesen: Der Aufruf welcher Codestelle verursacht in welchem Kontext mit welchen Parametern ein Problem?

Zudem fördert diese Terminologie das Prinzip von TDD, pro Unittest nur einen einzelnen Aspekt zu testen – versucht man, mehrere Aspekte in einem Unittest zusammen zu fassen, führt dies unweigerlich zu unhandlichen Methodennamen.

Nachdem ich auf dieser Basis nun eine Reihe von Unittests geschrieben habe, bemerke ich, dass sie mir bereits sehr ans Herz gewachsen sind. Interessant fand ich vor allem, dass ich allein durch das nachträgliche Schreiben von Unittests einen Fehler in einer zehnzeiligen Methode entdeckt habe, der trotz dem häufigen Einsatz dieser Methode in den vergangenen zwölf Monaten nicht aufgefallen ist.

Allein das gute Gefühl, zukünftig Änderungen und Erweiterungen an dieser Methode durchführen zu können, ohne Angst haben zu müssen, diesen Fehler unbemerkt wieder einzubauen, war die vergangenen drei Wochen allemal wert.

Insofern kann ich – auch wenn das Schreiben von Unittests für mich noch nicht intuitiv geschieht – guten Gewissens behaupten, dass Unittests sehr wohl Sinn machen. Dies zugebenermaßen sogar mehr, als ich noch vor vier Wochen gedacht hätte.

.NET Professionals im Profil: David Tielke

Sonntag, 15. November 2009, 09:37 Uhr
Permalink | Kommentare (1) | Kommentare als RSSRSS

David Tielke lebt in Winterberg, studiert Informatik an der Universität Marburg und beschäftigt sich vorrangig mit den Themen Softwarequalität und -entwicklung. Zudem gibt er sein Wissen über .NET im Rahmen des Microsoft Student Partner-Programms an andere Studenten weiter. Sie erreichen ihn über sein Blog.

Golo Roden: David, wie bist Du zur Softwareentwicklung gekommen? Wie und wann hast Du angefangen?

David Tielke: Ich habe im Laufe meines Bachelorstudiums gemerkt, dass einerseits zu wenig aktuelle Softwaretechnikinhalte behandelt wurden, und dass auf der anderen Seite die wenigen Inhalte, die es gab, vom Niveau her derart einfach gestaltet wurden, dass auch die BWL-lastigen Studiengänge diese hören konnten.

Man hatte zwar Grundlagen in Softwaretechnik und Programmierung gehört, allerdings gelang es weder mir noch meinen Kommilitonen eine „Brücke“ zwischen den beiden Bereichen zu schlagen. Irgendwann habe ich dann gemerkt, dass dies nicht besser wurde und begonnen, das ganze Thema in meiner Freizeit aufzuarbeiten.

Zu Beginn stand die Auswahl der Programmierplattform, und nachdem ich ein bisschen über Java und .NET recherchiert hatte, fiel die Wahl sehr deutlich auf .NET und C# als Sprache.

Als ich erste Erfolge beim Programmieren verzeichnen konnte, entwickelte sich eine wahre „Sucht“ nach Themen im Bereich .NET und den verwandten Microsoft-Technologien. Da das sehr viel Zeit kostete, kündigte ich meine Assistentenstelle beim Professor und suchte mir eine Stelle als .NET-Softwareentwickler.

Diese fand ich auch sehr schnell bei der Firma uniserve in Meschede, so dass ich nun quasi mein neues Hobby zum Beruf machen, endlich richtige Projekte entwickeln und damit auch noch mein Studium finanzieren konnte.

Schnell merkte ich aber, dass zu Softwareentwicklung weit mehr gehört als nur das Programmieren, und so versuchte ich, meine Projekte nach den Methoden durchzuführen, die ich in dem Fach Softwaretechnik gelernt hatte – leider mit mäßigem Erfolg.

Zum gleichen Zeitpunkt empfahl mich mein Professor, bei dem ich die Stelle gekündigt hatte, für eine Bachelor-Thesis bei einer Firma, die ebenfalls jüngst zu der Erkenntnis gekommen war, dass zu Softwareentwicklung mehr als Programmieren gehört.

Dort schrieb ich dann für ein halbes Jahr meine Bachelorarbeit und dabei konnte sehr viel über professionelle Softwareentwicklung aus allen möglichen Blickwinkeln lernen. Außerdem hatte ich viel Zeit, mich in die Bereiche Softwareprojektmanagement und Softwarequalität einzuarbeiten. Besonders die Methoden zur expliziten und impliziten Messung der Softwarequalität wie Unittests, Profiling, Metriken, ... haben mich sehr begeistert.

Da ich von 8 bis 17 Uhr für die eine, von 19 bis 0 Uhr für die andere Firma gearbeitet habe, habe ich dann versucht, das gelernte in meine Projekte bei uniserve einzubringen. Dort konnte ich dann von Woche zu Woche sehen, wie nicht nur die Qualität meiner Software besser wurde, sondern wie auch das ganze Projektmanagement schlagartig besser funktionierte.

Nach wenigen Wochen wurde mir klar, dass ich in meinem bald anstehenden Masterstudium mehr zu diesen Themen lernen wollte und fand nach kurzer Zeit einen entsprechenden Studiengang in Marburg, der genau diese Inhalte abdeckte.

Zeitgleich entdeckte ich das Microsoft Student Program, in welchem Microsoft eine kleine Gruppe .NET-interessierter Studenten mit Literatur, Lehrgängen und Software unterstützte, und diesen zudem die Möglichkeit gab, sich zertifizieren zu lassen. Nachdem die Bachelorarbeit beendet und mein Masterplatz in Marburg gesichert war, wurde ich auch im Student Program angenommen.

Heute habe ich mich in Marburg auf Softwarequalität und -entwicklung in verteilten Systemen spezialisiert, im Rahmen des Student Programs reise ich durch Deutschland und halte Workshops an Universitäten und Fachhochschulen, um andere Studenten auch für .NET – eine geniale Technologie – begeistern zu können, und baue schließlich bei uniserve systematisch eine Software für Kameramanagementsysteme auf.

Vor zwei Monaten wurde ich als Microsoft Certified Professional zertifiziert, und lerne derzeit für meine Microsoft Certified Technology Specialist Prüfung. Darüber hinaus versuche ich nach meinem Studium einen Promotionsplatz im Bereich „Softwarequalität in agilen Vorgehensmodellen“ zu bekommen, da in diesem Bereich die Forschung noch nicht weit fortgeschritten ist – aber bis dahin ist es noch ein langer Weg.

Golo Roden: Ein wesentliches Merkmal der IT ist, dass man beständig mit neuen Entwicklungen konfrontiert wird, und diesen folgen muss. Woher nimmst Du die Motivation, Dich quasi jeden Tag weiterzubilden und mit Neuem zu beschäftigen?

David Tielke: Mir fällt das Lernen sehr leicht, wenn ich mich für etwas begeistert habe. Bei .NET geht es sogar so weit, dass ich mir selbst die Frage stellen muss, wie ich verhindere, dass meine Motivation nicht überhandnimmt.

Früher als Kind und als Jugendlicher habe ich mich immer gefragt, wie bestimmte Dinge in der IT-Welt funktionieren, und mir vorgestellt, dass so etwas unglaublich kompliziert sein muss. Heute kann ich mit Stolz sagen, Teile dieser Welt zu beherrschen, und ich freue mich und habe unglaublichen Spaß daran, jeden Tag neue Dinge in diesem Bereich lernen und mein Wissen somit Stück für Stück aufbauen zu können.

Jeder, der den Entschluss fasst, in dieser Branche zu arbeiten, sollte sich der Tatsache bewusst sein, dass die Halbwertszeit von Wissen in der Softwareentwicklung nur ein bis zwei Jahre beträgt und man somit lebenslang neue Technologien lernen muss beziehungsweise darf. Die Frage ist nur, wie man an diese Herausforderung herangeht: Ich mache es mit Spaß und Begeisterung!

Golo Roden: Wenn sich ein Anfänger heute mit dem Thema Softwareentwicklung befassen will – welche Voraussetzungen sollte er Deiner Meinung nach dafür mitbringen, und was siehst Du als No-Go an?

David Tielke: Wie bei allen Themen ist es am wichtigsten, dass man Spaß und Begeisterung für die Thematik mitbringt. Ich kenne viele Softwareentwickler, für die das ganze „nur ein Beruf“ ist, und ich sehe seit Jahren, wie sie immer auf der Stelle treten und sich kaum noch weiterentwickeln, während junge und motivierte Entwicklerkollegen sie bereits eingeholt oder gar überholt haben.

Ebenfalls ist der Spaß am Lesen sehr wichtig: Ich denke, kaum ein guter Entwickler kommt ohne das regelmäßige Studieren von Büchern aus. Da dies meist in der Arbeitszeit nicht machbar ist, sollte man auch in seiner Freizeit gewillt sein, das ein oder andere Buch durchzuarbeiten – auch wenn man dafür nicht bezahlt wird. Aber auch hier gilt wieder: Einige „müssen“ diese Bücher durchlesen, andere „dürfen“ es. Ich zähle mich zu den letzeren.

Man sollte sehr früh versuchen, Praxiserfahrung zu sammeln, weil es etwas anderes ist, nach einem Pflichtenheft zu entwickeln, als nach Vorstellungen, die man selbst nur im Kopf hat. Die ganzen Dinge, die man in Vorlesungen in der Uni gelernt hat, sind nutzlos, wenn man nicht selbst am Kunden gesehen hat, wie schnell sich in einer Software alles ändern kann und wie praktisch es dann plötzlich ist, wenn man beispielsweise im Strategypattern nur eine Strategy ersetzen muss, um das Verhalten des ganzen Systems zur Laufzeit zu verändern.

Dabei ist es wichtig, immer am Ball zu bleiben und sich nicht entmutigen zu lassen, egal, wie kompliziert eine Technologie ist, oder wie lang die Debugsessions dauern. Man sollte seine Projekte und Arbeiten dabei sehr selbstkritisch betrachten, weil besonders bei Softwareprojekten immer Verbesserungen möglich sind.

Insbesondere sollte man auch Feedback und Kritik von anderen dankend aufnehmen und es als Chance sehen, sich weiterzuentwickeln. Wer seine Arbeit nicht gern zur Kritik stellt und mit Feedback nicht richtig umgehen kann, wird nie ein guter Softwareentwickler werden.

Golo Roden: Bei der Vielzahl an Technologien, die es heute gibt: Womit sollte ein Anfänger heutzutage anzufangen?

David Tielke: Wichtig ist auf jedem Fall, zunächst ein solides Fundament in C# und vor allem in objektorientierter Programmierung zu haben. Das Thema der Klassen und Interfaces fand ich besonders am Anfang eine Hürde, aber dank zahlreicher freier Literatur zu dem Thema im Internet ist das mit etwas Geduld zu meistern.

Danach sollte man zu einer Technologie greifen, die keine weiteren Vorkenntnisse voraussetzt, wie zum Beispiel Windows Forms als UI-Framework. Dort können dann all die Konzepte von C# wie Klassen, Events, Delegates, ... wiederholt und gefestigt werden. Auf diese Art hat man danach auf jeden Fall ein ausreichendes Grundverständnis für alle weiteren Technologien.

Des Weiteren sollte man auf jeden Fall einen kleinen Abstecher zu XML machen, weil das eine Technologie ist, auf die sehr viele andere Frameworks zurückgreifen und man einfach überall in .NET mit XML in Berührung kommt, beispielsweise bei den Konfigurationsdateien.

Danach sollte man die weiteren Punkte nach persönlicher Neigung angehen. Für fast alles weitere kommt man um das Erlernen einer oder mehrerer Zusatzpunkte nicht herum – XAML für WPF und Silverlight; SOAP für WCF; HTML, CSS und JavaScript für ASP.NET; SQL für ADO.NET; ...

Hierbei bietet es sich an, eine Technologie zu wählen, bei der man die eine oder andere Hilfstechnologie bereits beherrscht, oder bei der das Erlernen der Zusatzpunkte sehr leicht fällt.

Golo Roden: Du hast Dich auf Softwarequalität spezialisiert – wie kam es dazu? Und welche Rolle misst Du diesem Thema für Anfänger zu?

David Tielke: Zunächst fand ich bereits in den Univorlesungen das Testen von Software eine sehr spannende Sache, weil man so jederzeit in der Lage war, die Korrektheit des Gesamtsystems zu testen. Genau an dem Punkt, an dem ich in meinen eigenen Projekten feststellte, wie wichtig das gesamte Thema ist, kam mein Professor mit der Bachelorthesis zu dem Thema auf mich zu und, wodurch ich mich immer weiter in dieses Gebiet vertiefte.

Ich finde es beeindruckend, wie man mit Methoden und Techniken den stark kreativen Prozess der Softwareentwicklung an die gleiche Qualität wie bei der herkömmlichen industriellen Fertigung heranführen kann.

Auch wenn das Thema nicht direkt zu Beginn bei einem Anfänger auf der Liste stehen sollte, so kommt man daran über kurz oder lang nicht vorbei. Spätestens, wenn man in ein Team kommt, das schon seit Jahren qualitativ hochwertige Software entwickelt, muss man sich mit dem Thema Softwarequalität auseinandersetzen.

Allerdings ist es ebenso wichtig, erst einmal in die vielen Fallen zu treten, die da draußen in der Softwarewelt ausliegen, damit man wirklich versteht, welchen Sinn bestimmte Techniken haben.

Golo Roden: Welchen Rat würdest Du einem Anfänger abschließend mit auf den Weg geben?

David Tielke: Als Anfänger sieht man oft den Wald vor lauter Bäumen nicht. Leider gibt es für Einsteiger nur sehr wenig Literatur, meist wird man zudem direkt zu Beginn mit Techniken und Abkürzungen erschlagen und verliert dann ein wenig die Motivation.

Hier ist es wichtig, am Ball zu bleiben, denn nur die wenigsten verstehen alle Konzepte direkt von Anfang an. Zumeist liest man etwas und stolpert dann zu einem späteren Zeitpunkt in einem Projekt über eine Problemstellung die eine solche Lösung erfordert. Dann erinnert man sich meist und kann mit etwas Nachlesen das Problem lösen.

Falls man Schüler oder Student ist, sollte man versuchen, eine Nebentätigkeit zu bekommen – besonders für Studenten gibt es sehr viele Nebenjobs als Softwareentwickler – oder sich an Opensource-Projekten zu beteiligen, um möglichst schnell „richtige“ Praxiserfahrung zu sammeln. Das bringt nämlich wesentlich mehr als zu Hause im stillen Kämmerlein zu entwickeln.

Auch wenn Programmieren wichtig ist, muss man sich darum kümmern, auch die theoretischen Themen zu beherrschen, da diese für die Entwicklung von qualitativ hochwertiger Software enorm wichtig sind.

Wie in allen anderen Branchen ist auch in der Softwareentwicklung noch kein Meister vom Himmel gefallen. Wichtig ist, dass man sich stetig weiterentwickelt – der Rest kommt von ganz allein.

Performanceimplikationen von System.Transactions

Montag, 9. November 2009, 14:23 Uhr
Permalink | Kommentare (0) | Kommentare als RSSRSS

In meinem Blogeintrag Transaktionale Methoden habe ich vorgestellt, wie mit Hilfe von PostSharp ein TransactionalAttribute für .NET implementiert werden kann, das sich analog zu der @Transactional-Annotation von JEE verhält. Intern habe ich dabei den in .NET 2.0 eingeführten Namensraum System.Transactions verwendet.

Nun wurde als Kommentar zu diesem Blogeintrag die Frage gestellt, ob die Klasse TransactionScope generell verwendet werden könne, oder ob man besser auf die klassischen ADO.NET-Transaktionen zurückgreifen sollte, weil diese angeblich performanter seien.

Als Antwort auf diese Frage habe ich folgenden Kommentar verfasst:

die kurze Antwort lautet: Sofern Du den SQL Server einsetzt, kannst Du guten Gewissens auf System.Transactions setzen, bei anderen Datenbanken ist zumindest Vorsicht geboten.

Wie komme ich zu dieser Aussage?

Vor der Einführung des Namensraums System.Transactions in .NET 2.0 gab es zwei verschiedene Möglichkeiten, Transaktionen zu verwenden:

  • Einerseits konnten die klassischen ADO.NET-Transaktionen verwendet werden, die programmatisch angesprochen und gesteuert wurden.
  • Andererseits konnten die Enterprise Services verwendet werden, um deklarativ Methoden zu kennzeichnen, die transaktional ausgeführt werden sollten.

Beide Verfahren haben ihre jeweiligen Vor- und Nachteile: Da die klassischen ADO.NET-Transaktionen direkt auf der Datenbank ausgeführt werden, sind sie sehr performant, sind aber für verteilte Transaktionen ungeeignet.

Transaktionen, die hingegen über die Enterprise Services ausgeführt werden, verwenden implizit den Distributed Transaction Coordinator (DTC) von Windows und sind daher für verteilte Transaktionen geeignet. Da der DTC jedoch immer eingesetzt wird – auch dann, wenn eine lokale Transaktion genügen würde – ist die Performance nicht ganz so hoch wie bei den klassischen ADO.NET-Transaktionen.

Der Namensraum System.Transactions dient nun als Nachfolger für beide Varianten, weshalb prinzipiell so wohl lokale wie auch verteilte Transaktionen unterstützt werden. Die Logik, nach welcher der DTC hinzugezogen wird (oder eben nicht), ist dabei folgendermaßen aufgebaut:

  • Wenn eine Verbindung zum SQL Server aufgebaut wird, wird die Transaktion zunächst lokal ausgeführt. Sobald innerhalb der gleichen Transaktion eine weitere Verbindung zu einer Datenbank aufgebaut wird, wird die Transaktion verteilt ausgeführt. Damit dies funktioniert, enthält der SQL Server ein Feature namens Transaction Propagation, womit eine lokale in eine verteilte Transaktion umgewandelt werden kann.
  • Wenn eine Verbindung jedoch zu einer anderen Datenbank als einem SQL Server aufgebaut wird, geht .NET davon aus, dass Transaction Propagation nicht unterstützt wird, weshalb von System.Transactions direkt eine verteilte Transaktion gestartet wird – unabhängig davon, ob noch eine weitere Verbindung zu einer anderen Datenbank hergestellt wird oder nicht.

Die Konsequenz: Sofern innerhalb einer Transaktion nur eine einzelne Verbindung zu einem SQL Server hergestellt wird, verhält sich System.Transactions analog zu der klassischen ADO.NET-Transaktion – und verfügt damit über dieselbe Performance.

Der Overhead des DTC schlägt erst bei verteilten Transaktionen oder beim Einsatz einer anderen Datenbank zu.

Insofern kann man die Verwendung von System.Transactions beim Einsatz von SQL Server guten Gewissens generell empfehlen – bei allen anderen Datenbanken ist hingegen Vorsicht geboten, da hier automatisch der DTC zum Einsatz kommt, auch wenn keine verteilten Transaktionen genutzt werden.

Auch Kirchen haben Kragsteine

Samstag, 7. November 2009, 18:41 Uhr
Permalink | Kommentare (3) | Kommentare als RSSRSS

Vor knapp einer Woche habe ich mit meinem Blogeintrag Wie viel Sinn machen Unittests? – der im Rahmen der monatlichen Streitgespräche von Peter Bucher und mir erschienen ist – für einige Verwunderung gesorgt. Viele haben sich gefragt, warum gerade ich Unittests dermaßen skeptisch gegenüberstehe.

Ralf Westphal hat in seinem Kommentar vermutet, dass

etwas hinter den drei “problematischen Aspekten” das Grundproblem zu sein [scheint]

und ich muss zugeben, dass diese Vermutung nicht von der Hand zu weisen ist. Als problematisch habe ich an Unittests im Wesentlichen die Tatsache betrachtet, dass Code zum Zweck einer besseren Testbarkeit potenziell angepasst werden muss, da sich einige Konstrukte ansonsten nur schlecht oder gar nicht testen lassen.

Als Beispiel seien an dieser Stelle abstrakte Klassen angeführt, die per Definition nicht instanziiert und deshalb nicht direkt getestet werden können. Prinzipiell kann dieses Problem auf zwei Arten gelöst werden:

  • Entweder werden die bereits bestehenden abgeleiteten Klassen getestet – immerhin ist die abstrakte Basisklasse in diesen implizit enthalten.
  • Alternativ wird eine dedizierte abgeleitete Klasse erstellt, welche die abstrakte Basisklasse beerbt – die Basisklasse ist also auch in dieser Variante implizit enthalten.

Der relevante Unterschied zwischen beiden Varianten liegt in der Tatsache, dass die bereits bestehenden abgeleiteten Klassen eigene Funktionalität mitbringen, die den Test nicht unbedingt verfälschen, aber zumindest beeinflussen könnte: Es wird schwierig, den Test auf die abstrakte Basisklasse zu beschränken.

Eine dedizierte Klasse verfügt nicht über diese Nachteile: Sie sorgt lediglich dafür, dass die abstrakte Basisklasse in möglichst natürlicher Form instanziiert werden kann.

Technisch ist dieses Vorgehen unproblematisch, aber mental bereitete es mir Probleme: Seit Jahren wird Entwicklern vor allem in der akademischen Welt impliziert, dass Code nur dann “gut” ist, wenn er kompakt, elegant und im mathematischen Sinne schön ist.

Wie sind nun Änderungen, die nur um einer besseren Testbarkeit durchgeführt werden, mit diesem Paradigma vereinbar? Die kurze, aber schmerzhafte Antwort lautet: Gar nicht.

Wie bereits in Wie viel Sinn machen Unittests? angesprochen, beschreibt auch Roy Osherove in seinem Buch The Art of Unit Testing in dem Abschnitt Overcoming the encapsulation problem dieses Problem:

Some people feel that opening up the design to make it more testable is
a bad thing because it hurts the object-oriented principles the design is
based on. I can wholeheartedly say to those people, “Don’t be silly.”

Seine zugegebenermaßen sehr pragmatische Lösung

“Don’t be silly.”

dieses Problems stellt für mich keine zufriedenstellende Antwort dar – schließlich kommen die Bedenken nicht von ungefähr, und diese Antwort spiegelt dies in keinerlei Hinsicht wieder.

Auch die im Gespräch mit einigen anderen Entwicklern geäußerte Ansicht

Das ist halt so.

ist nicht zufriedenstellend. Diese Aussage beschreibt nämlich lediglich den Status Quo, begründet aber nicht, warum es in Ordnung oder vielleicht sogar gut ist, auf diese Art vorzugehen. Letztlich spiegelt diese Aussage lediglich das resignierte Abfinden mit einer – potenziell – unbequemen Tatsache dar.

Doch nun hat Ralf Westphal eine Begründung geliefert, die den Ansprüchen der Skeptiker – zumindest aus meiner Sicht – genügt:

das ist völlig ok. maschinen haben ja auch revisionsklappen für wartungsarbeiten. die haben sonst keine funktionalität. oder denk an kragsteine an alten kirchen, damit man gerüste anbringen kann.

Nach einigen Jahren des Zweifels und der Skepsis empfinde ich diese Analogie als eine wahre Erlösung – endlich wird begründet, warum ein solches Vorgehen in der Software in Ordnung ist.

Man mag einwenden, dass dieser Vergleich zur realen Welt doch offensichtlich und naheliegend sei, doch war in den vergangenen Jahren sonst niemand, mit dem ich mich über dieses Thema unterhalten habe, in der Lage, einen ähnlich klaren, einleuchtenden Vergleich zu ziehen.

Persönlich bemerke ich, dass diese Erkenntnis eine Art Wendepunkt darstellt – die mentale Barriere, mit der ich mich bislang gegen Unittests gesträubt habe, weil sie Anpassung von Code erfordern, ist aufgelöst. Alles, was bleibt, sind technische Probleme – und diese können mit Sicherheit gelöst werden.

Als Konsequenz werde ich das umsetzen, was Ralf Westphal vorgeschlagen und empfohlen hat:

Unit Tests […] konsequent und kompetent mal für einen Monat einzusetzen. Mit der Kompetenz mag es da schwierig sein, weil man sich dann eine Veränderung der Handlungs- und Denkgewohnheit selbst beibringen muss... aber das ist zumindest ein Anfang.

Wie es mir damit ergeht – darüber werde ich berichten …

Wie viel Sinn machen Unittests?

Sonntag, 1. November 2009, 09:37 Uhr
Permalink | Kommentare (22) | 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. November 2009, ist es nun wieder so weit, und unser Thema für diesen Monat lautet:

Wie viel Sinn machen Unittests?

So wohl Peter wie auch ich haben uns unabhängig voneinander im Vorfeld unsere Gedanken gemacht, wie wir diesem Thema gegenüberstehen. Außerdem nimmt diesen Monat auch Christian Wenz wieder an unserem Streitgespräch teil.

Peters und Christians Kommentare findet sich zeitgleich in den entsprechenden Blogs, folgend nun mein Kommentar zu diesem Thema:

Unittests sind sinnvoll – nicht ohne Grund wird ihr konsequenter Einsatz empfohlen, unter anderem im Rahmen der Clean Code Developer-Initiative und des Extreme Programmings, von der dediziert auf Tests optimierten Entwicklungsmethode Test-Driven Development (TDD) ganz zu schweigen.

In der Theorie hören sich diese Ansätze zunächst auch sehr vielversprechend an. Schließlich ermöglicht der konsequente, durchgängige Einsatz von Unittests eine gefahrlose Änderung von bestehendem Code.

Unbeabsichtigte Seiteneffekte werden durch die Ausführung der Unittests sofort aufgedeckt und können somit vermieden werden. Zudem können gefundene Bugs unmittelbar durch neu geschriebene Unittests abgesichert werden, so dass sie sich nicht erneut einschleichen können.

So weit die Theorie, die Praxis sieht leider ein wenig anders aus. So leicht Unittests nämlich erklärt sind, so kompliziert ist ihre tatsächliche Umsetzung – vorausgesetzt, man strebt tatsächlich eine 100%ige Abdeckung an.

Problematisch sind in der Regel drei Aspekte:

  • Der bestehende objektorientierte Aufbau ist unter OOP- und stilistischen Aspekten sauber umgesetzt, läuft einer guten Testbarkeit allerdings zuwider.
  • Es bestehen zahlreiche Abhängigkeiten zu externen Komponenten, die sich nicht oder nur mit sehr viel Aufwand simulieren lassen.
  • Es bestehen Abhängigkeiten zur konkreten Laufzeitumgebung, die sich nicht oder nur mit sehr viel Aufwand nachbilden lassen.

Entwickler, die sich an Clean Code Developer orientieren,und die auch ansonsten versuchen, beispielsweise mit Werkzeugen wie FxCop ihren Stil und ihre Codequalität zu verbessen, streben in der Regel ein sauberes und durchdachtes objektorientiertes Design an.

Leider steht manchmal genau ein solches Design der Testbarkeit gegenüber. Häufig müssen beispielsweise im Sinne der Testbarkeit weitere Konstruktoren, zusätzliche Eigenschaften oder Schnittstellen zu einer Klasse hinzugefügt werden – obwohl dies aus einer rein objektorientierten Sicht keinen Sinn ergibt.

Roy Osherove beschreibt in seinem Buch The Art of Unit Testing in dem Abschnitt Overcoming the encapsulation problem dieses Problem:

Some people feel that opening up the design to make it more testable is
a bad thing because it hurts the object-oriented principles the design is
based on. I can wholeheartedly say to those people, “Don’t be silly.”

Leider kann ich mich seiner leichtfertigen Aussage “Don’t be silly” nicht so einfach anschließen – denn ein Unittest ist eben nicht einfach nur ein weiterer Verwender der API. Für den Unittest ist es nämlich im Gegensatz zu realen Entwicklern gleichgültig, ob eine API sinnvoll strukturiert ist oder nicht.

Daher empfinde ich es zumindest als bedenklich, ein durchdachtes, objektorientiertes und sauberes Design zu Gunsten einer besseren Testbarkeit zu ändern, wenn es dafür keine weiteren Gründe gibt.

Neben dem potenziell wenig Unittest-tauglichen objektorientierten Design stellt die Abhängigkeit von externen Komponenten ein weiteres häufiges Problem dar. Zu nennen sind hierbei unter anderem:

  • Datenbanken
  • Dateisysteme
  • Registry

Natürlich können all diese Abhängigkeiten entfernt, umgangen oder verschleiert werden – die Frage ist aber, zu welchem Preis dies geschieht. Es genügt nämlich nicht, Stubs und Mocks einzuführen, statt dessen muss die Umgebung auch nach jedem Test potenziell wieder zurückgesetzt werden: Schließlich soll jeder Test isoliert, das heißt unabhängig von seinen Vorgängern, ausgeführt werden.

Eine Datenbank wieder und wieder zurückzusetzen, ist technisch zwar machbar, kostet allerdings sehr viel Zeit. Damit wird das Ziel, alle Unittests bei jedem Build automatisch auszuführen, ab einem gewissen Punkt unerreichbar: Die zur Ausführung der Unittests benötigte Zeit liegt schlichtweg zu hoch.

Zudem können gar nicht alle externen Abhängigkeiten ohne weiteres aufgelöst werden – vor .NET 3.5 SP1 und der Einführung von System.Web.Abstractions war es zum Beispiel nur eingeschränkt möglich, den HTTP-Kontext einer Webanwendung zu simulieren.

Auch die Abhängigkeit von der Laufzeitumgebung an sich kann problematisch sein. So können sich beispielsweise HTTP-Anforderungen, die an eine Webanwendung gesendet werden, voneinander unterscheiden – je nachdem, welcher Webbrowser auf dem Client eingesetzt wird.

Um Webbrowser-spezifisches Verhalten zu simulieren, müssten die Eigenheiten des jeweiligen Webbrowsers im Unittest nachgebildet werden. Alternativ könnten der entsprechenden gewünschte Webbrowser automatisiert werden – was seinerseits allerdings einen ziemlichen Aufwand nach sich zieht.

Ebenfalls problematisch ist multithreaded Code, sofern bestimmte Konstellationen in den einzelnen Threads getestet werden sollen – da das Umschalten zwischen den Threads vom aktuellen Kontext des Prozessors beziehungsweise des Betriebssystems abhängt, kann sich das Verhalten von Ausführung zu Ausführung unterscheiden.

All diese Beispiele zeigen, dass es unter Umständen sehr aufwändig oder gar unmöglich sein kann, einen Unittest zu schreiben, der unter realitätsnahen Bedingungen ausgeführt wird.

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.

Nichtsdestotrotz gibt es natürlich auch Code, der sich perfekt für Unittests eignet: So fallen beispielsweise sämtliche Algorithmen in diese Kategorie, da diese in der Regel kaum oder gar keine Abhängigkeiten zu anderen Komponenten aufweisen.

Daher ergibt es durchaus Sinn, Unittests zu schreiben – die Frage ist lediglich, wofür. Eine 100%ige Abdeckung mit Unittests erreichen zu können klingt zwar verlockend, der zur Erreichung dieses Ziels notwendige Aufwand lohnt in der Regel jedoch nicht.

Insofern muss im konkreten Kontext abgewogen werden, welche Komponenten als kritisch zu betrachten sind und der Abdeckung durch Unittests bedürfen. Selbst für diese Komponenten ist eine 100%ige Abdeckung noch ein ausgesprochen ehrgeiziges Ziel – daher empfiehlt es sich eher, eine zwar hohe, aber nicht perfekte Abdeckung wie beispielsweise 85% anzustreben.

Wie viel Sinn machen Unittests also nun? Zusammengefasst kann man sagen, dass Unittests – an der richtigen Stelle eingesetzt – durchaus Sinn ergeben, dass diese Stellen aber explizit ausgewählt werden sollten.