27. Juli 2018
Martin Maier
0

Testgetriebene Entwicklung (TDD) einer Angular-Anwendung

 

 

Grafische Oberflächen von komplexen Systemen wurden lange Zeit hauptsächlich mit soliden, schwergewichtigen Technologien in sehr hoch entwickelten, objektorientierten Sprachen realisiert. Dabei wurden meist Fat Clients durch WPF oder Swing entwickelt. Eher seltener handelte es sich dabei um Thin Clients in Form von Webanwendungen. HTML und JavaScript wurden in diesen Fällen vorwiegend durch große Frameworks wie ASP oder JSF abstrahiert und serverseitig gerendert. Der Sprachkern von JavaScript entwickelte sich jedoch mit ECMAScript 2015 stark weiter und wurde dadurch auch für komplexere Anwendungen interessant. Basierend auf den Vorschlägen zu ECMAScript 2015 entstand mit TypeScript zusätzlich eine Möglichkeit, Webanwendungen auf sehr hohem sprachlichen Niveau und statisch typisiert zu entwickeln. Durch diese starken sprachlichen Verbesserungen und den starken Support durch Giganten wie Facebook und Google, wurden Webtechnologien zunehmend für komplexere Anwendungen in Form von Singe Page Applications (SPA) interessant.

Leider hat der Quellcode solcher auf Webtechnologien basierender Anwendungen oft noch den Ruf, sich hinsichtlich Lesbarkeit, Design und Testabdeckung auf einem qualitativ sehr niedrigen Level zu befinden. Der Wert qualitativ hochwertigen Code zu entwickeln, um eine Anwendung nachhaltig effizient weiterzuentwickeln und warten zu können, ist jedoch technologieunabhängig und bedarf damit verbundener Prinzipien und Methoden um diesen Wert erhalten.

Testgetriebene Entwicklung (Test Driven Development = TDD) ist eine Methode aus dem Extreme Programming (XP), um qualitativ hochwertigen und gut getesteten Code zu entwickeln.

Dabei wird in kurzen Iterationen stets zuerst in Tests das Verhalten spezifiziert, bevor es implementiert und anschließend die Lesbarkeit und Struktur des Codes verbessert wird. Dies führt nicht nur zu gut testbarem und getestetem Code, sondern auch dazu, dass sich Entwickler bereits sehr früh mit der Struktur bzw. dem Design des Codes auseinandersetzen müssen. Zudem wird durch die vorgelagerte Spezifizierung durch Tests auch effektiv nur Funktionalität entwickelt, die tatsächlich gefordert wird und schützt somit davor prädiktiven und unnötig komplexen Code zu entwickeln.

Mit der Angular CLI erstellte Projekte, enthalten „out of the box“ alle Bordmittel um testgetrieben entwickeln zu können.

Dieses Tutorial soll anhand eines einfachen Szenarios beschreiben, wie Angular Projekte testgetrieben entwickelt werden können.

Das Szenario

Wir wollen den Fokus auf die Methodik legen und wählen deswegen ein sehr einfaches fachliches Szenario. Mit der Anwendung sollen rudimentär Kontakte verwaltet werden können. Sie besteht grundlegend aus nur zwei Funktionalitäten:

  • Kontakte anzuzeigen
  • Neue Kontakte anzulegen

Aufsetzen des Angular Projekts

Ein Angular Projekt ist mittels Angular CLI zügig aufgesetzt.

 

 

Wir möchten die Anwendung von Grund auf entwickeln. Dazu

  • löschen wir Komponente app.component
  • bereinigen die davon betroffenen Abhängigkeiten app.module und index.html
  • löschen die End-to-End Spezifikationen im Verzeichnis e2e/src
  • und überprüfen unsere Änderungen indem wir das Projekt mit ng build bauen

Der „Walking Skeleton“

Unser System ist sehr klein, kommuniziert mit keinem Backend und wird einfachheitshalber auch nicht automatisiert ausgeliefert. Damit handelt es sich bei unserem Skelett ausschließlich um einen Durchstich zur testgetriebenen Entwicklung unserer Anwendung. Dieser sollte einen E2E-Test umfassen, welcher über „Page Objects“ mit der Webanwendung interagiert, und einer ersten kleinen Komponente, welche testgetrieben entwickelt wurde. Eine passende erste kleine Story, um den „Walking Skeleton“ zu erstellen, ist das Anzeigen der Kontakte.

Vorgehen

Wir entwickeln in kurzen TDD-Iterationen. Um dieses Tutorial lesbarer und übersichtlicher zu gestalten, verwende ich ein paar Icons, um den Ablauf darzustellen. Als erstes definieren und sprechen wir darüber, was wir in der nächsten Iteration erreichen möchten. Bestenfalls entwickeln wir das nicht alleine, sondern haben uns mit einem anderen Entwickler „gepaired“. Wir spezifizieren einen entsprechenden Test, starten ihn  und beobachten wie er fehlschlägt  . Daraufhin implementieren wir die Funktionalität und starten den Test erneut  bis dieser erfolgreich ist . Wir überprüfen unseren Code Style, führen Refactorings durch und starten danach mit der nächsten kleinen TDD-Iteration.

Spezifizierung der Story in einem ersten E2E-Test

Wir definieren den Kontext der Story als Kontaktverwaltung und erstellen dementsprechend eine E2E-Spezifikation /e2e/src/contact-management.e2e-spec.ts.

Wir legen fest, dass die Webseite der Kontaktverwaltung initial die beiden Kontakte „Max“ und „Moritz“ auflistet.

Innerhalb der E2E-Spezifikationen wollen wir auf einer sehr hohen Ebene mit der Anwendung interagieren und delegieren den Zugriff auf die HTML-Elemente deswegen in sogenannte „Page Objects“.

In TDD-Manier schreiben wir unseren E2E-Test als würde eine Klasse ContactManagementPage bereits bestehen, um sie so zu definieren, wie sie aus Client-Sicht am sinnvollsten ist.

Wir erstellen uns eine Instanz der ContactManagementPage, navigieren auf deren Ansicht und stellen sicher, dass dort nur die Kontakte „Max“ und „Moritz“ aufgelistet werden.

 

 

Sind wir damit soweit fertig, erstellen wir die Klasse ContactManagementPage unter /e2e/src/pageobjects/contact-management.po.ts, wie im Testfall angenommen.

 Nun kompiliert der Testfall, schlägt aber – wie erwartet – fehl.

Wir beginnen mit der Implementierung des „Page Objects“ bei der Method #navigateTo, welche zur Kontaktverwaltungs-Ansicht navigiert. In unserem Fall ist das aktuell “/”.

Als nächstes benötigen wir die Namen der aufgelisteten Kontakte. An dieser Stelle definieren wir, dass die Ansicht Elemente der Klasse „contact“ darstellen soll, welche wiederrum ein Element der Klasse „name“ beinhalten.

Wir implementieren also #shownNamesOfContacts indem wir alle Namens-Elemente heraussuchen und deren Textinhalte mappen.

Protractor arbeitet asynchron mit eigenen Promises. Wir müssen also unbedingt darauf achten, die richtige Promise-Klasse von Protractor zu importieren.

 

 

   Der Test schlägt nun fehl mit einem Hinweis, dass keine Angular Seite gefunden werden konnte. Der Grund dafür ist, dass aktuell keine Angular Komponente bootstrapped wird.

Um diesen Fehler zu beheben erstellen wir als nächstes eine Angular Komponente “contact-list” mit Hilfe der Angular CLI.

ng generate component contact-list

Sie wird automatisch dem globalen App-Modul zugeordnet und dort deklariert. Das ist für dieses Tutorial soweit in Ordnung, aber wir müssen diese Komponente auch noch in unsere index.html einpflegen und im Modul konfigurieren, dass die Komponente auch gebootstrapped wird.

   Der E2E-Test schlägt weiterhin fehl, diesmal jedoch wie gewünscht, dadurch dass die Erwartungen nicht erfüllt wurden.

Implementierung der Komponente

Zunächst soll die Komponente eine Liste mit einem Eintrag je Kontakt darstellen.

Wir erstellen uns dafür einen Testfall in der von der Angular CLI mitgenerierten contact-list.component.spec wieder unter der Annahme, dass benötigte Abhängigkeiten bereits existieren.

 

 

Der Code kompiliert nicht. Wir benötigen das Daten Objekt „Contact“ und eine Property „contacts“ im Controller contact-list.components.ts der Komponente, welches die Kontakte bereitstellt.

Wir erstellen also die Klasse /model/contact.ts

 

 

und erweitern den Controller um die Property contacts: Contact[].

   Wir starten diesmal die Karma Tests durch ng test und sehen, dass der eben geschriebene Test fehlschlägt, da wir zwei Kontakt-Elemente erwarten, jedoch keiner dargestellt wird.

Wir können Karma im Hintergrund laufen lassen und bekommen dadurch sofortiges Feedback über unsere Änderungen am Code und deren Auswirkungen auf die Testergebnisse.

Wir erweitern die View contact-list.component.html dahingehend, eine Liste mit einem Eintrag je Kontakt darzustellen.

 

 

 Die Tests sind erfolgreich!

So weit so gut. Nun wollen wir die Namen der Kontakte anzeigen.

 

 

   Der Test schlägt – wie erwartet – fehl.

Wir erweitern also den View, der den Namen des Kontakts darstellt.

 

 Erfolgreich! Unsere Komponente zeigt nun die Namen der Kontakte an.

Ist die Story damit fertig implementiert? Wir lassen die E2E-Tests laufen.

   Dieser schlägt weiterhin fehl. Wir erwarten, dass “Max” and “Moritz” initial als Kontakte angezeigt werden.

Als nächstes wollen wir also, dass die Komponente initial die Kontakte „Max“ und „Moritz“ darstellt.

 

   Der Test schlägt fehl.

Wir initialisieren das Property des Controllers mit den entsprechenden Kontakten.

 Alle Komponenten-Tests sind erfolgreich.

 Nun ist auch der E2E-Test grün und die Implementierung der Story damit abgeschlossen.

 

Weiterentwicklung

Da nun unser „Walking Skeleton“ steht, können wir mit der Weiterentwicklung fortfahren.

Mögliche nächste Schritte könnten beispielsweise sein:

  • Refactoring: Auslagern und Delegierung der „contact-list“ spezifischen Inhalte in ein eigenes „Page Object“, um das ContactManagementPage-Object übersichtlich zu halten und die „contact-list“ auch in anderen E2E-Tests wiederverwenden zu können.

  • Story: Anlegen neuer Kontakte
    • Über ein Textfeld soll beim Betätigen der Enter-Taste ein neuer Kontakt angelegt werden.

    • Wurde ein neuer Kontakt angelegt, soll das Textfeld zurückgesetzt werden.

  • Optimierung: Auslagern der Datenhaltung in einen Redux-Store

 

Fazit

Frameworks wie Jasmine, Karma oder Protractor unterstützen die testgetriebene Entwicklung von Webanwendungen sehr gut.

Im Falle von Angular sind Projekte, die mit der Angular CLI erstellt wurden, bereits mit Jasmine, Karma und Protractor vorkonfiguriert und können „out of the box“ testgetrieben entwickelt werden.

TDD ist also auch für die Entwicklung von Webanwendungen eine sehr gute Praxis, um qualitativ hochwertigen und gut getesteten Quellcode zu generieren.

Im Zusammenspiel mit anderen Praktiken wie Pair-Programming und Clean Coding ergeben sich sehr gute Synergieeffekte, wodurch Systeme sehr effektiv und effizient weiterentwickelt und gewartet werden können.

Martin Maier

Martin Maier ist als Senior Consultant für Softwareentwicklung bei der Saxonia Systems AG in München tätig. Sein Schwerpunkt liegt bei der Entwicklung von Java- und Webanwendungen. Er ist überzeugter Software Craftsman und entwickelt leidenschaftlich gerne testgetrieben und gepaired. Seine Freizeit verbringt er am liebsten mit seiner Familie, Wing Tsun und dem Lesen von Literatur rund um die Welt der Softwareentwicklung.

Twitter 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0