Testgetriebene Entwicklung mit AngularJS

JavaScript für den Enterprise-Einsatz

Philipp Tarasiewicz, Robin Böhm
© Shutterstock/ra2studio

Kaum eine Konferenz oder Fachzeitschrift hat sich des Themas AngularJS noch nicht angenommen. Auch diverse Schulungs- und Projektanfragen bestätigen: AngularJS ist in Deutschland angekommen. Und das nicht zu Unrecht…

AngularJS ist ein JavaScript-Framework für die Entwicklung von modernen clientseitigen Webanwendungen. Erstmalig halten bewährte Konzepte wie Dependency Injection und konsequente testgetriebene Entwicklung Einzug in die JavaScript-Welt und machen diese somit attraktiv für den Enterprise-Einsatz. Das Framework bietet hier einige sehr intuitive APIs und achtet in hohem Maße auf das Einhalten und Bilden von Schnittstellen. In den beiden vorangegangenen Ausgaben haben wir bereits ein AngularJS-Projekt mit dem Namen Cube angelegt. Cube ist eine einfache Anwendung zur Visualisierung eines dreidimensionalen Würfels, bei dem wir den X-, Y- und Z-Rotationsgrad steuern können. In der vorherigen Ausgabe haben wir das Projekt so erweitert, dass Würfel nun auch dynamisch erstellt und wieder gelöscht werden können. Außerdem gibt es die Möglichkeit, die sechs Seiten eines Würfels mit thematischen Hintergrundbildern auszustatten (Abb. 1). Wir haben uns dafür entschieden, diese Bilder über das Flickr-API zu beziehen. Aus der technischen Perspektive betrachtet haben wir uns also bereits die folgenden AngularJS-Konzepte angesehen: Zwei-Wege-Datenbindung, Templates, Scopes, Controller, Direktiven und Services.

In dieser Ausgabe wollen wir in erster Linie anschneiden, welche Möglichkeiten uns das Framework bietet, um testgetrieben entwickeln zu können. In AngularJS wird das Thema der Testbarkeit nämlich ganz groß geschrieben. Der Frameworkcode selbst zeigt eine außerordentlich hohe Testabdeckung auf, und auch alle Anwendungskomponenten wurden so konzipiert, dass wir sie sehr gut testen können. Die Ausrede, dass clientseitige Webanwendungen schwierig zu testen seien, greift also nicht mehr.Bevor wir uns an das Testthema wagen, wollen wir unsere Cube-Anwendung zunächst jedoch noch um zwei Aspekte erweitern. Dazu werden wir im ersten Schritt unseren flickr-Service mithilfe eines Serviceproviders definieren und den Service somit konfigurierbar machen. Außerdem werden wir Routen einbauen, um zu zeigen, wie wir aus einer AngularJS-Anwendung eine vollwertige Single-Page-Applikation machen können.

Abb. 1: Stand des Cube-Projekts aus dem zweiten Teil

Serviceprovider und Konfiguration

In der vorangegangenen Ausgabe haben wir den flickr-Service implementiert, der die HTTP-Kommunikation zum Flickr-API kapselt und uns somit einen einfachen Zugriff auf die Hintergrundbilder erlaubt. Um den Servicebegriff nicht unnötig kompliziert einzuführen, haben wir uns dort für die einfache Definition mithilfe einer Service-Factory (factory()) entschieden. Das ist die häufigste Art, einen Service zu definieren. Allerdings hat sie einen entscheidenden Nachteil. Services, die mit einer Factory definiert werden, können nicht konfiguriert werden. Und wenn wir nochmals über unseren flickr-Service nachdenken, sollte uns auffallen, dass es zumindest eine Eigenschaft gibt, für die sich eine Konfiguration anbietet, nämlich für den API-Key für das Flickr-API. Somit wollen wir uns nun anschauen, wie wir die bestehende Factory (flickrService.js) in einen Provider überführen können, bei dem der API-Key konfigurierbar ist.

Konfigurations- und Ablaufphase
Jede AngularJS-Anwendung durchläuft beim Starten grob betrachtet zwei Phasen. Zunächst befindet sich die Anwendung in der Konfigurationsphase. Das ist die Phase, in der die registrierten Provider und config()-Blöcke verarbeitet werden. In dieser Phase wurden insbesondere noch keine Services instanziiert. Das ist auch der Grund dafür, warum wir uns in den config()-Blöcken lediglich die Provider übergeben lassen können (und nicht die daraus resultierenden Services). Nach der Konfigurationsphase wird die Anwendung in die Ablaufphase überführt. Dafür werden dann insbesondere die aus den Providern resultierenden Services instanziiert und die für die Ansichten nötigen Templates kompiliert. Der Benutzer kann anfangen, mit der Anwendung zu interagieren.

Den Quellcode für den Serviceprovider sehen wir in Listing 1. Wir können erkennen, dass wir einen Provider mit der provider()-Funktion von AngularJS einführen können. Wie alle anderen Komponenten zuvor auch definieren wir den Provider in unserem Anwendungsmodul cubeApp. Auch die provider()-Funktion erwartet zwei Parameter. Als ersten Parameter geben wir den Namen des Service an, den wir mithilfe des Providers erstellen wollen. Mit dem zweiten Parameter legen wir die Konstruktionsvorschrift des Providers fest, indem wir eine anonyme Funktion übergeben. Wir können nachvollziehen, dass wir innerhalb der anonymen Funktion mit this arbeiten. Das liegt daran, dass AngularJS die anonyme Funktion intern als „Klasse“ betrachtet und somit mithilfe des new-Operators ein neues Providerobjekt von dieser Klasse erzeugt.

Klassen in JavaScript
In JavaScript können wir mithilfe von Funktionen und Prototypen ein Konstrukt nachbilden, das einer Klasse ziemlich nahekommt. Innerhalb einer Klasse, die mithilfe einer Funktion definiert wurde, können wir – genauso wie in Java – mit this auf das entsprechende Objekt zugreifen. Allerdings funktioniert das nur, wenn die Funktion tatsächlich in Verbindung mit dem new-Operator aufgerufen wird. Solche Funktionen nennen wir auch Konstruktorfunktionen. Wenn eine Konstruktorfunktion ohne den new-Operator aufgerufen wird, dann setzt die Laufzeitumgebung den Wert von this innerhalb der Funktion auf das globale Objekt, was im Falle eines Browsers das window-Objekt ist. Mit speziellen JavaScript-Funktionen wie call() und apply() haben wir allerdings die Möglichkeit, den Wert von this beim Aufruf auch manuell festzulegen.

Es gilt die Konvention, dass AngularJS letztendlich den Teil als Service registriert, den der Provider in seiner $get()-Funktion zurückgibt. Das bedeutet, dass die $get()-Funktion praktisch der anonymen Funktion entspricht, die wir in der vorangegangenen Ausgabe der factory()-Funktion als zweiten Parameter übergeben haben. Jede andere Funktion, also in diesem Fall lediglich die apiKey()-Funktion, dient zur Konfiguration und kann somit nur in der Konfigurationsphase aufgerufen werden. Mit der apiKey()-Funktion können wir in der Konfigurationsphase also abhängig vom übergebenen Parameter lesend bzw. schreibend auf den API-Key zugreifen. Das ist im Übrigen ein Muster, das wir häufig in der JavaScript-Welt wiederfinden werden. Ein und dieselbe Funktion wird also als Setter wie auch als Getter genutzt.

angular.module("cubeApp")
.provider("flickr", function () {
    var API_KEY;

    this.apiKey = function (apiKey) {
      if (apiKey) {
        API_KEY = apiKey;
      }
      return apiKey;
    };


    this.$get = function ($http) {
      var getPhotosByTagFn = function (tag) {
        return $http.get("http://api.flickr.com/services/rest/", {
            params: {
              method: "flickr.photos.search",
              api_key: API_KEY,
              [...]
            }
        })
        .then( [...] );

      };

      // Public API
      return {
        getPhotosByTag: function (tag) {
          return getPhotosByTagFn(tag);
        }
      };
    }
});
angular.module("cubeApp")
.config(function (flickrProvider) {
    flickrProvider.apiKey("4e0e99bf242015aee01bffea1efff314");
});

Jetzt haben wir mithilfe des Providers eine Möglichkeit geschaffen, um unseren flickr-Service konfigurieren zu können. Was bleibt, ist die Frage, wie denn nun die eigentliche Konfiguration des API-Keys durchgeführt werden kann. Maßgeblich dafür sind die so genannten config()-Blöcke (Listing 2), die AngularJS in der Konfigurationsphase ausführt. In einem config()-Block können wir uns per Dependency Injection die zu konfigurierenden Provider übergeben lassen, um die entsprechenden Konfigurationsfunktionen aufrufen zu können (in diesem Fall die apiKey()-Funktion). Wichtig ist an dieser Stelle, dass wir uns tatsächlich den flickrProvider übergeben lassen und nicht den eigentlichen flickr-Service. Letzteres würde ohnehin nicht funktionieren, weil der Service an dieser Stelle noch nicht existiert und erst nach der Konfigurationsphase aus dem zugehörigen Provider hervorgeht. Bezüglich der Codeorganisation erstellen wir in dem Verzeichnis app/scripts/ das Unterverzeichnis mit dem Namen config und legen die flickrConfig.js aus Listing 2 dort ab. Bei der Einbindung in unserer index.html sollten wir darauf achten, dass die Datei flickrService.js vor der flickrConfig.js eingebunden wird, da der Provider ansonsten in dem config()-Block nicht bekannt ist.

[...]
<script src="scripts/module.js"></script>
<script src="scripts/directives/cubeDirective.js"></script>
<script src="scripts/controllers/cubeListCtrl.js"></script>
<script src="scripts/services/flickrService.js"></script>
<script src="scripts/config/flickrConfig.js"></script>

Aufmacherbild: Modern cyber soldier with target matrix eye concept von Shutterstock / Urheberrecht: ra2studio

[ header = Seite 2: Single-Page-Applikation durch Routen ]

Single-Page-Applikation durch Routen

Nachdem wir unseren flickr-Service mithilfe eines Providers definiert haben, wollen wir jetzt noch Routen in unsere Anwendung einbauen. Routen machen aus einer Anwendung eine vollwertige Single-Page-Applikation, weil somit definierte Anwendungszustände über einen URL erreicht werden können. Damit wird also auch das so genannte Deep Linking möglich.
Wir wollen zwei Routen erstellen. Über die Root-Route (“/”) soll weiterhin unsere Liste von Würfeln erreichbar sein. Aber außerdem wollen wir eine zweite Route anbieten (“/:tag”), mit der wir einen einzigen rotierenden Würfel aufrufen können. Das Tag für das Thema der Hintergrundbilder soll dabei als URL-Pfadparameter übergeben werden können.
Um Routen einsetzen zu können, müssen wir uns zunächst das Modul ngRoute (angular-route.js) hier herunterladen und im Verzeichnis app/lib/angular-route/ ablegen. Außerdem dürfen wir nicht vergessen, es in unserer index.html nach dem AngularJS-Framework einzubinden:

<script src="lib/angular/angular.js"></script>
<script src="lib/angular-route/angular-route.js"></script>
<script src="scripts/module.js"></script>
[...]

Darüber hinaus müssen wir jetzt noch festlegen, dass das ngRoute-Modul eine Abhängigkeit unseres cubeApp-Moduls ist. Das erledigen wir in der module.js durch einen entsprechenden Eintrag in dem Array mit Abhängigkeiten:

angular.module('cubeApp', ['ngRoute']);

Jetzt können wir anfangen, unsere Routen zu konfigurieren. Dazu benötigen wir den $routeProvider, den das ngRoute-Modul mitbringt. Wir legen in dem Verzeichnis app/scripts/config/ eine weitere Konfiguration mit dem Namen routeConfig.js an. Auch diese muss – wie gewöhnlich – in der index.html eingebunden werden. Den Inhalt der Konfiguration sehen wir in Listing 3. Wir lassen uns also den $routeProvider übergeben und konfigurieren mithilfe der when()-Funktion, dass die Root-Route (“/”) auf das Template views/cubeList.html und den Controller CubeListCtrl abgebildet werden soll. Mithilfe von otherwise() konfigurieren wir außerdem, dass der Benutzer zur Root-Route weitergeleitet werden soll, falls es keine Übereinstimmung gibt. Somit müssen wir den HTML-Code aus unserer index.html, der die Würfelliste einschließlich der Eingabemaske definiert, in die Datei views/cubeList.html auslagern. Außerdem sollten wir die ngController-Direktive, die das <body>-Tag annotiert, entfernen. Nach diesem Refactoring sollte die index.html nun so aussehen wie in Listing 4.

angular.module("cubeApp")
.config(function ($routeProvider) {
    $routeProvider
    .when("/", {
        controller: "CubeListCtrl",
        templateUrl: "views/cubeList.html"
    })
    .otherwise({
        redirectTo: "/"
    });
});

Wie uns in Listing 4 sicherlich aufgefallen ist, arbeiten wir in der index.html mit der ngView-Direktive. Diese Direktive ist ebenfalls Teil des ngRoute-Moduls und definiert das DOM-Element, unterhalb dessen die Templates aus den Routen eingefügt werden sollen.

<!DOCTYPE html>
<html ng-app="cubeApp">
  <head>
    <title>Cube</title>
    <link href="styles/cube.css" media="all" rel="stylesheet">
  </head>
  <body>


    <div ng-view></div>


    <script src="lib/angular/angular.js"></script>
    <script src="lib/angular-route/angular-route.js"></script>
[...]

  </body>
</html>

Wenn wir unsere Anwendung nun im Browser neu laden, sollte sich nichts verändert haben, bis auf die Tatsache, dass wir unsere Würfelliste nun durch jede beliebige Route erreichen können. Dafür haben wir ja mithilfe von otherwise() eine entsprechende Weiterleitung zur Root-Route konfiguriert.

Eine zweite Route mit URL-Pfadparameter

Wie zuvor erwähnt, wollen wir jetzt noch eine zweite Route („/:tag“) einbauen, die einen rotierenden Würfel anzeigt, dessen Tag für die Hintergrundbilder wir über einen URL-Pfadparameter übergeben können. Die nötige Erweiterung in der routeConfig.js sieht wie in Listing 5 aus.

$routeProvider
.when("/", {
    controller: "CubeListCtrl",
    templateUrl: "views/cubeList.html"
})
.when("/:tag", {
    controller: "SingleTagCubeCtrl",
    templateUrl: "views/singleTagCube.html"
})
.otherwise({
    redirectTo: "/"
});

Somit müssen wir in dem Verzeichnis app/scripts/controllers/ noch die Datei singleTagCubeCtrl.js für den SingleTagCubeCtrl-Controller (Listing 7) und natürlich auch das entsprechende Template in der Datei views/singleTagCube.html (Listing 8) erstellen. Die Datei singleTagCubeCtrl.js muss natürlich – wie üblich – in unserer index.html eingebunden werden. Außerdem können wir in der Datei cubeList.html noch eine kleine Erweiterung machen, um die neue Route aus der Würfelliste heraus in einem neuen Fenster aufrufen zu können (Listing 6).

<li ng-repeat="cube in cubes">
  <cube init-x="{{cube.x}}"
    init-y="{{cube.y}}"
    init-z="{{cube.z}}"
    tag="{{cube.tag}}"></cube>
  <button class="removeCube" ng-click="removeCube($index)">
    remove this cube
  </button>
  <a ng-href="#/{{cube.tag}}" target="_blank">
    Open {{cube.tag}} in a new window
  </a>
</li>
angular.module("cubeApp")
.controller("SingleTagCubeCtrl", function ($scope, $routeParams) {
    $scope.paramTag = $routeParams.tag;
});

In dem SingleTagCubeCtrl-Controller können wir auf den URL-Pfadparameter tag zugreifen, indem wir uns den $routeParams-Service von AngularJS per Dependency Injection übergeben lassen (Listing 8).

<div class="center">
  <h1>{{paramTag}}</h1>
  <cube class="rotate" init-x="5" init-y="26" init-z="101" tag="{{paramTag}}"></cube>
</div>

Abb. 2: Aufruf der neuen Route mit URL-Pfadparameter „niagara“

[ header = Seite 3: Testen in AngularJS ]

Testen in AngularJS

Wie eingangs bereits erwähnt, ist AngularJS in allen Belangen vor dem Hintergrund der Testbarkeit konzipiert worden. Das bedeutet, dass es für jede Komponente (Service, Controller, Direktive, Filter usw.) ein Testkonzept gibt und entsprechende Mechanismen existieren, um Tests mit wenig Aufwand implementieren zu können. Wer also der Meinung ist, dass clientseitige Webanwendungen schwierig zu testen seien, der sollte sich über diese Tutorialreihe hinaus mit AngularJS intensiver beschäftigen. Es ist klar, dass wir in diesem Rahmen nicht alle Einzelheiten zum Testen vorstellen können, denn damit könnten wir sicherlich auch Bücher füllen. Dennoch wollen wir einige Kernanspekte anschneiden, um die Attraktivität von AngularJS in Hinblick auf das Testen herauszustellen.

Grundsätzlich unterscheiden wir in AngularJS zwischen Unit Tests und so genannten End-to-End-Tests (kurz: E2E-Tests). Mit Unit Tests testen wir isoliert die kleinsten sinnvollen Einheiten unserer Software auf Codeebene. Für gewöhnlich gehören die typischen Anwendungskomponenten wie Services, Direktiven, Filter oder Controller zu solchen Einheiten. Durch den Einsatz von Dependency Injection haben wir die Möglichkeit, die Abhängigkeiten dieser Komponenten durch Mock-Objekte auszutauschen. AngularJS bringt für viele interne Komponenten sogar Mock-Implementierungen mit, mit denen die Definition von Testfällen noch einfacher wird. In E2E-Tests betrachten wir unsere Anwendung hingegen als Blackbox. Wir haben die Möglichkeit, ein Benutzerverhalten zu skripten und formulieren auf Basis des DOMs entsprechende Erwartungen, die ausdrücken, wie sich unsere Anwendung verhalten soll. Das bedeutet, dass wir mit E2E-Tests auch ganze Szenarios ausdrücken können.

Toolchain zur Testausführung
Um die Tests auszuführen, benötigen wir Karma, eine Testausführungsumgebung für JavaScript-Anwendungen. Karma benötigt Node.js als Laufzeitumgebung. Der Node.js-Installer kann hier für alle gängigen Plattformen heruntergeladen werden. Nachdem wir Node.js installiert haben, können wir Karma mit dem folgenden Konsolenbefehl installieren:

npm install -g karma

Um nun die Testfälle auszuführen, müssen wir in dem Verzeichnis, in dem die Karma-Konfiguration liegt, den folgenden Befehl ausführen:

karma start

Dabei erwartet Karma, dass die Konfiguration den Dateinamen karma.conf.js trägt. Häufig haben wir in unseren Projekten allerdings mehrere Konfigurationen (z. B. für die speziellen Testarten jeweils eine). In solchen Fällen können wir beim Aufruf noch explizit eine Konfigurationsdatei angeben:

karma start karma-e2e.conf.js

Weitere Informationen zu Karma finden wir auf der offiziellen Webseite.

Testen des „flickr“-Services

Im ersten Teil des Tutorials (Ausgabe 3.2014) haben wir bereits in dem Hauptverzeichnis unserer Anwendung das Unterverzeichnis test erstellt. Wie wir dem Namen entnehmen können, werden wir in diesem Verzeichnis unsere Tests ablegen. Für gewöhnlich legen wir in dem test-Verzeichnis noch zwei Unterverzeichnisse unit und e2e für die beiden Testarten an.
Wir wollen uns nun um die Tests für den flickr-Service kümmern. Dazu erstellen wir in dem Verzeichnis test/unit/ noch ein weiteres Unterverzeichnis mit dem Namen services. Wie wir der Ordnerstruktur entnehmen können, gehört es zum guten Ton, die Tests genauso zu strukturieren wie die eigentliche Applikation. In dem services-Verzeichnis erstellen wir nun die Datei flickrService.spec.js. Auch hier können wir wieder eine Best Practice erkennen. Der Dateiname entspricht dem Dateinamen des Service mit dem kleinen Unterschied, dass er mit .spec.js endet anstatt mit .js. Damit ist klar, dass sich in dieser Datei tatsächlich Tests befinden. Zur Definition der Tests setzen wir das Framework Jasmine ein. Es bringt viele Konzepte mit, die Java-Entwickler bereits aus JUnit kennen werden. Eine ausführliche Beschreibung der Möglichkeiten von Jasmine finden Sie hier.
Eine neue Testsuite leiten wir mit der describe()-Funktion ein. Sie erwartet als ersten Parameter eine Beschreibung der Suite und als zweiten Parameter eine Funktion, die den Aufbau der Suite festlegt. Einen konkreten Testfall definieren wir mit der it()-Funktion. Auch diese Funktion erwartet als ersten Parameter eine Beschreibung des Testfalls und als zweiten Parameter eine Funktion, die die Testlogik enthält. Darüber hinaus können wir in einer Testsuite mithilfe von beforeEach() und afterEach() eine Logik jeweils vor bzw. nach jedem Testfall ausführen. Beide Funktionen erwarten als einzigen Parameter eine Funktion, die die auszuführende Logik beschreibt. Wir sollten außerdem erwähnen, dass eine Testsuite auch weitere Testsuiten enthalten kann. Somit besitzen wir ein ziemlich flexibles Konzept zur Strukturierung unserer Testfälle. Es sei angemerkt, dass wir hier nur die interessanten Teile der Testsuite zum flickr-Service besprechen. Die vollständige ausführbare Suite ist im GitHub Repository zur Tutorialreihe hier zu finden. Auch die Karma-Konfiguration (karma.conf.js), die wir zum Ausführen der Testfälle benötigen, ist dort zu finden.
Das Interessante an unserem flickr-Service ist, dass er mithilfe des $http-Service HTTP-Anfragen an das Flickr-API stellt. Sinnvolle Unit Tests für einen Service zu schreiben, der über HTTP mit einem Web Service kommuniziert, ist für gewöhnlich nicht so einfach, weil wir relativ viel Quellcode zur Implementierung eines Mock-Objekts schreiben müssen. Zum Glück bietet uns AngularJS auch dafür eine Lösung.
Um die HTTP-Anfragen tatsächlich auszuführen, nutzt der $http-Service intern eine Komponente mit dem Namen $httpBackend. Im Test können wir diese Komponente gegen eine vom Framework mitgebrachte Mock-Implementierung austauschen, die noch einige weitere Features mitbringt. Dazu gehört das Formulieren von so genannten vordefinierten HTTP-Antworten (Trained Responses) und die Definition von so genannten Anfrageerwartungen (Request Expectations). Auf diese Weise können wir einerseits die HTTP-Antwort eines Web Service beliebig faken und andererseits überprüfen, ob eine Komponente (hier: der flickr-Service) unter bestimmten Bedingungen tatsächlich eine HTTP-Anfrage an einen bestimmten URL stellen würde. Weitere Informationen dazu finden Sie hier.

Damit uns AngularJS im Test das $httpBackend-Mock-Objekt zur Verfügung stellt, müssen wir in der Karma-Konfiguration das Modul ngMock einbinden, welches hier heruntergeladen werden kann. In der Beispielkonfiguration im GitHub Repository haben wir das bereits erledigt. Das ngMock-Modul bringt außerdem die beiden Funktionen module() und inject() mit. Mithilfe der module()-Funktion laden wir unser Anwendungsmodul cubeApp, damit die dort definierten Komponenten (u. A. der flickr-Service) im Test zur Verfügung stehen. Die inject()-Funktion macht die Dependency Injection innerhalb von Tests nutzbar. Einen Ausschnitt der Testsuite für den flickr-Service sehen wir in Listing 9. Wir können erkennen, dass wir mithilfe eines beforeEach()-Blocks vor jedem Testfall unser Anwendungsmodul cubeApp laden und über den flickrProvider einen fiktiven API-Key festlegen. In einem zweiten beforeEach()-Block lassen wir uns den instanziierten flickr-Service und das $httpBackend-Mock-Objekt übergeben und weisen sie den lokalen Variablen flickr bzw. $httpBackend zu, damit sie in allen Testfällen zur Verfügung stehen. AngularJS versteht im Test bei der Angabe von Abhängigkeiten auch die Schreibweise mit einem Unterstrich vor und nach dem Komponentennamen (z. B. _flickr_), um der Variablenüberschattung entgegenzuwirken. Die eigentlichen Testfälle, die wir in Listing 9 sehen können, sind eher struktureller Art. Wir überprüfen, ob die getPhotosByTag()-Funktion tatsächlich ein Array zurückgibt und ob die zurückgegebenen URLs für die Hintergrundbilder der erwarteten Form entsprechen. Um im Test mit der Asynchronität eleganter umgehen zu können, bietet uns das $httpBackend-Mock-Objekt die Funktion flush(). Mit dem Aufruf dieser Funktion können wir synchron steuern, wann die HTTP-Antwort erhalten werden soll. Dadurch, dass der $http-Service auf Promises basiert, wird somit auch das entsprechende Promise synchron aufgelöst. Das bedeutet, dass wir keinerlei komplexere Mechanismen von Jasmine nutzen müssen, um mit der Asynchronität zurechtzukommen.

var flickr, $httpBackend, flickrResponse={ ... };

beforeEach(module('cubeApp', function (flickrProvider) {
      flickrProvider.apiKey("API_KEY");
}));

beforeEach(inject(function (_flickr_, _$httpBackend_) {
      $httpBackend = _$httpBackend_;
      flickr = _flickr_;
}));

it("should transform response to an array", function () {
    var responseArray;
    flickr.getPhotosByTag("animal")
    .then(function (response) {
        responseArray = response;
    });
    $httpBackend.flush();

    expect(responseArray instanceof Array).toBe(true);
});

it("should transform responseArray to correct flickr imgUrls", function () {
    var responseArray;
    flickr.getPhotosByTag("animal")
    .then(function (response) {
        responseArray = response;
    });
    $httpBackend.flush();

    var imgUrlFormat = "http://farm{farm-id}.staticflickr.com/{server-id}/{id}_{secret}.jpg",
    imgUrl = imgUrlFormat
    .replace("{farm-id}", flickrResponse.photos.photo[0].farm)
    .replace("{server-id}", flickrResponse.photos.photo[0].server)
    .replace("{id}", flickrResponse.photos.photo[0].id)
    .replace("{secret}", flickrResponse.photos.photo[0].secret);

    expect(imgUrl).toBe(responseArray[0]);
});

Zusammenfassung

Wir haben uns in diesem Teil des Tutorials angesehen, wie wir den flickr-Service mithilfe eines Serviceproviders anstatt einer Service-Factory definieren können. Damit haben wir eine Möglichkeit geschaffen, um den API-Key für das Flickr-API in der Konfigurationsphase unserer Anwendung konfigurieren zu können. Außerdem haben wir Routen eingebaut und somit aus der Anwendung eine vollwertige Single-Page-Applikation gemacht. Mit den konfigurierten Routen kann ein Benutzer also eine bestimmte Ansicht und einen definierten Anwendungszustand per Deep Link erreichen. Im Falle der /:tag-Route ist das eine Ansicht, die einen rotierenden Würfel mit den Hintergrundbildern anzeigt, die thematisch dem übergebenen Tag entsprechen. Außerdem haben wir verdeutlicht, dass AngularJS ausdrücklich vor dem Hintergrund der Testbarkeit konzipiert wurde und somit für alle Anwendungskomponenten ein Testkonzept existiert. Anhand des flickr-Service haben wir einige Testfälle kurz angeschnitten, um ein Gefühl dafür zu vermitteln, wie so etwas in der Praxis aussehen kann. Das ganze ausführbare Beispiel inklusive aller Testfälle finden Sie hier. Viel Spaß beim Ausprobieren und Nachbauen!

Wie geht es weiter?

Wir hoffen, dass wir Ihnen in dem Umfang dieser dreiteiligen Tutorialreihe einen Einblick in die Entwicklung mit AngularJS geben konnten. Natürlich war es uns in dem Kontext nicht möglich, auf alle Einzelheiten und Features im Detail einzugehen. Aber es sollte Ihnen klar geworden sein, warum das Framework der neue Stern am Himmel der JavaScript-Welt ist. Falls wir Ihr Interesse hiermit geweckt haben und Sie sich näher mit diesem Thema beschäftigen wollen, sei Ihnen an dieser Stelle unser Buch „AngularJS – Eine praktische Einführung in das JavaScript-Framework“ ans Herz gelegt (hier bestellbar). Außerdem betreiben wir hier das Portal AngularJS.de, wo es neben einer Auflistung unserer zukünftigen Workshops und Vorträge auch viele interessante Informationen zum Thema gibt. Natürlich können Sie uns auch jederzeit für eine Inhouseschulung buchen.

Geschrieben von
Philipp Tarasiewicz
Philipp Tarasiewicz
Philipp Tarasiewicz ist freiberuflicher Technologieberater, Autor und Coach im Umfeld des Webs. Seit einigen Jahren hat er sich auf den Bereich Enterprise JavaScript, insbesondere AngularJS, spezialisiert und konzipiert auf dessen Basis moderne Softwarelösungen. Zusammen mit Robin Böhm hat er das erste deutschsprachige Buch zu AngularJS geschrieben (dpunkt.verlag).
Robin Böhm
Robin Böhm
Robin Böhm ist freiberuflicher Berater, Coach und Autor im Umfeld moderner Webtechnologien mit dem Fokus auf JavaScript, AngularJS und testgetriebener Entwicklung. Er ist Mitgründer des deutschen Portals AngularJS.DE (http://angularjs.de) und hat zusammen mit Philipp Tarasiewicz das erste deutschsprachige Buch zum Thema AngularJS (dpunkt.verlag) geschrieben.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: