Suche
Test-getriebene Entwicklung mit Angular 2, TypeScript und IDE-Support

Angular 2 mit NetBeans und TypeScript testen

Karsten Sitterberg

© Shutterstock/Marijus Auruskevicius

Dieser Artikel baut auf der bereits veröffentlichen Einführung zu Angular 2, TypeScript und NetBeans auf. Wer diese verpasst hat, sollte damit beginnen, die Einführung nachzulesen. Der vorliegende Artikel konzentriert sich auf in TypeScript erstellte Unit-Tests für Angular 2 Komponenten mit Jasmine und Karma.

Jede Anwendung sollte – unabhängig davon, ob es sich um eine native oder eine Webanwendung handelt – durch automatisierte Tests gegen Fehler und Regressionen abgesichert werden. Dabei unterscheidet man im wesentlichen zwischen zwei Formen von Tests: Ende-zu-Ende-Tests, die eine Anwendung als Ganzes im Kontext einer Testumgebung prüfen und Unit-Tests, die kleine Teile der Anwendung isoliert betrachten. Im Folgenden werden Unit-Tests vorgestellt.

Unit-Tests dienen dazu, die einzelnen Bestandteile einer Anwendung (Komponenten) separat und voneinander isoliert zu testen.

Durch die Isolation wird der Aufwand für Tests reduziert, da nicht die gesamte Anwendung, inkl. Testdaten und Umsystemen, bereitgestellt werden muss, um dann anschliessend mit umfangreichen und komplexen Tests alle möglichen Ablaufpfade zu testen. Stattdessen werden nur die einzelnen Komponenten mit entsprechend weniger komplexen Tests geprüft.

Durch gute Unit-Tests, die optimalerweise vor der eigentlichen Implementierung geschrieben werden (Test-driven development), erzielt man sogar eine bessere Wiederverwendbarkeit und Erweiterbarkeit der Anwendung, denn um die Komponenten in Isolation testen zu können, müssen diese eine entsprechend gute Kapselung aufweisen. Die Tests in Kombination mit der produktiven Nutzung können dabei schon als eine Form der Wiederverwendung gesehen werden, da es bereits zwei Nutzer für die Komponenten gibt. Angular 2 unterstützt zusätzlich über das Modulsystem Dependency Injection und die Konzepte von Components und Services einen modularen Aufbau der Anwendung mit einer sauberen Trennung von Verantwortlichkeiten.

Bei der Formulierung von Tests kommen sehr viele Vergleiche vor. Diese können zwar durch die jeweils eingesetzte Programmiersprache zum Ausdruck gebracht werden, jedoch bieten Assertion Libraries ein ausdrucksstärkeres und besser lesbares API für die zu formulierenden Zusicherungen. Jasmine ist eine solche JavaScript-Bibliothek und wird im nächsten Abschnitt vorgestellt.

image00

Jasmine

Jasmine ist ein Open-Source-Framework, das dazu dient, in und für JavaScript Anwendungen zu entwickeln. Es ist in jeder Umgebung, die JavaScript ausführen kann, lauffähig und hat keine Abhängigkeit auf das DOM oder andere Browser-APIs. Jasmine stellt eine eigene Assertion Library zur Verfügung, die sowohl die Formulierung als auch das Lesen und Verstehen von Tests erleichtert.

Eine einfache Testsuite in Jasmine zeigt das folgende Beispiel: Eine Suite (describe) kann mehrere Testspezifikationen (it) enthalten. Jeder Test kann mehrere Annahmen (expect) beinhalten, die sich auf den Zustand der Anwendung beziehen. Jede Annahme wird dabei mit der Hilfe von Matchern formuliert, die im Ergebnis true oder false liefern. Solange die Rückgabe true ist, wird die Zusicherung als erfolgreich betrachtet.

Das folgende Beispiel für einen einfachen Test ist damit der triviale Erfolgsfall:

describe("A suite", function() {
  it("contains spec with an expectation", function() {
    expect(true).toBe(true);
  });
});

Mehr Informationen zu Jasmin sind auf der Projektwebseite verfügbar: http://jasmine.github.io.

Es gibt zu Jasmine noch die populären Alternativen mocha und QUnit. Da Jasmine offiziell durch Angular 2 unterstützt wird, wird Jasmine im weiteren Verlauf genutzt.

Jasmine-Tests können innerhalb des Webbrowsers ausgeführt werden, wo dann auch direkt die Ergebnisse betrachtet werden können. In kleinen Projekten mit wenigen Tests kann dieser Ansatz ausreichend sein. Für umfangreichere Projekte mit vielen Tests ist (build)-Automatisierung und IDE-Unterstützung jedoch notwendig. Hier kommt Karma als Test-Runner ins Spiel, der nun betrachtet wird.

image07

Karma

Karma ist ein sogenannter Test-Runner. Er erlaubt die automatische Ausführung von Tests auf verschiedenen Endgeräten und Browsern. Karma wurde ursprünglich vom AngularJS-Team entwickelt, um die bisher eingesetzten Ablaufumgebungen zu ersetzen: Durch den Einsatz von JavaScript-Umgebungen außerhalb echer Browser kam es zu vielen false-positives (Fehlermeldung; auch wenn in einem echten Browser keine Fehler auftreten) und es wurden umgekehrt Fehler nicht gemeldet, die in einem Browser auftreten. Daher wird bei Karma, anstatt einer eigenen JavaScript-Laufzeitumgebung, die echte Umgebung eines Browser (z.B. Firefox oder Chrome) genutzt. Der Vorteil von diesem Ansatz liegt auf der Hand: Die Tests werden in einer Umgebung durchgeführt, die genau so wie die spätere Produktivumgebung ist.

Karma liefert die Tests mit einem eigenen HTTP-Server aus und startet zur Testausführung den benötigten Webbrowser. Karma stellt dabei sicher, dass der Browser kontinuierlich zur Verfügung steht – stürzt der Browser ab oder wird anderweitig beendet, wird er erneut gestartet. In der Kombination mit Jasmine ist Karma für die Ausführung zuständig, Jasmine stellt die Library zur Formulierung der Unit-Tests bereit.

Mehr Informationen zu Karma gibt es auf der Projekthomepage: https://karma-runner.github.io.

Typings

Angular 2 Anwendungen können in der Programmiersprache TypeScript, einer getypten Erweiterung von JavaScript ES5 (und auch ES2015), geschrieben werden. Da ein großer Teil der Entwicklungsarbeit durch Tests bestimmt wird, sollten auch diese entsprechend in TypeScript entwickelt werden.

Jasmine gibt es derzeit lediglich als ES5-JavaScript-Bibliothek. Da TypeScript interoperabel mit JavaScript ist, ist das erstmal kein Problem. Jedoch fehlen dann die ganzen Vorteile von TypeScript, wie IDE-Support für Code Completion und statische Analyse, die durch die Typisierung ermöglicht werden.

Nutzt man TypeScript und verwendet darin Jasmine, so merkt die IDE bzw. der Compiler an, dass keine Informationen zu den Typen und Properties zur Verfügung stehen.

Um dies zu beheben, muss der TypeScript-Compiler mit Typinformationen versorgt werden. Der einfachste Weg, um die Typinformationen bereitzustellen, ist den TypeScript-Definitionsmanager mit dem Namen Typings zu verwenden. Eine Installation kann mittels NPM vorgenommen werden, Typings lässt sich dann mit zu NPM analogen Befehlen bedienen. Die zu installierenden Typings werden in der typings.json-Datei im jeweiligen Projekt konfiguriert.

Für das Beispiel sieht die Datei typings.json wie folgt aus, um Jasmine-Typings zu verwenden:

{
  "dependencies": {},
  "devDependencies": {},
  "ambientDevDependencies": {
        "jasmine": 
"github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#26c98c8a9530c44f8c801ccc3b2057e2101187ee"
  }
}

Die Installation der Typdefinitionen und von Typings wird am besten als Teil des npm install-Installationsschrittes konfiguriert, was mit den folgenden Zeilen in der package.json Datei geschieht:

"scripts": {
        ...
"postinstall": "typings install --ambient",
...
}

...

"devDependencies": {
...
            "typings": "^0.7.9",
...
} 

Nachdem alle Voraussetzungen für einen effizienten Einsatz von TypeScript für Jasmine-Tests geschaffen sind, kann die restliche Testumgebung eingerichtet werden.

Einrichtung der Testumgebung

Aufbauend auf dem Beispiel aus dem vorherigen Artikel sollte die vollständige package.json nun wie folgt aussehen:

{
  "name": "2016-01-angular2-simple",
  "version": "1.0.1",
  "author": "Karsten Sitterberg",
  "scripts": {
    "postinstall": "typings install --ambient",
    "pretest": "npm run tsc",
    "test": "karma start karma.conf.js",
    "clean": "rm -rf app/*.js* && rm -rf test/*.js*,
    "distclean": "rm -rf node_modules && rm -rf typings",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "start": "concurrent \"npm run tsc:w\" \"npm run lite\" ",
  },
  "dependencies": {
    "angular2": "2.0.0-beta.14",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.2",
    "zone.js": "0.6.6",
    "systemjs": "0.19.6"
  },
  "devDependencies": {
    "concurrently": "^1.0.0",
    "lite-server": "^1.3.1",
    "jasmine-core": "2.3.2",
    "karma": "^0.13.22",
    "karma-chrome-launcher": "^0.2.3",
    "karma-jasmine": "^0.3.8",
    "typings": "^0.7.9",
    "typescript": "^1.8.9"
  }
}

Im Vergleich zum vorherigen Artikel wurden die Versionen von Angular 2 und TypeScript aktualisiert sowie die Jasmine- und Karma-Abhängigkeiten ergänzt.

Im Abschnitt scripts wurden ein postinstall-Script zur Installation der Typings sowie test– und pretest-Scripts zur eigentlichen Ausführung der Tests ergänzt.

Die clean– und distclean-Scripts wurden erweitert, damit diese auch kompilierte Tests und heruntergeladene Typings-Dateien aufräumen.

An der Konfiguration des TypeScript-Compilers in der Datei tsconfig.json hat sich nichts geändert. Der vollständige Quellcode ist auf GitHub unter der folgenden URL abrufbar: codecoster/2016-angular-simple.

Im Wesentlichen konnten die Datei index.html und die Dateien der Anwendung unverändert gelassen werden, lediglich in der Datei boot.ts gibt es eine Anpassung:

Da seit Angular 2.0.0-beta.6 transitive Typings nicht mehr automatisch enthalten sind, müssen die ES6-Typings explizit hinzugefügt werden. Dies erfolgt mittels der speziellen Kommentarsyntax mit drei Slashes und einer Referenzdeklaration. Im Beispiel ist dies in der ersten Zeile der Datei boot.ts zu sehen:

/// <reference path="../node_modules/typescript/lib/lib.es6.d.ts" />
import {bootstrap}       from 'angular2/platform/browser'
import {SimpleComponent} from './simple.component'

bootstrap(SimpleComponent);

Die Karma-Konfiguration in der Datei karma.conf.js kommt ebenfalls neu hinzu und könnte beispielhaft wie folgt aussehen:

module.exports = function (config) {
        config.set({
            basePath: '.',
            frameworks: ['jasmine'],
            port: 9876,
            logLevel: config.LOG_INFO,
            colors: true,
            autoWatch: true,
            browsers: ['Chrome'],
            // Karma plugins loaded
            plugins: [
                'karma-jasmine',
                'karma-chrome-launcher'
            ],

            files: [
                // paths loaded by Karma
                {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true},
                {pattern: 'node_modules/es6-shim/es6-shim.js', included: true, watched: true},
                {pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true},
                {pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true},
                {pattern: 'node_modules/angular2/bundles/angular2.dev.js', included: true, watched: true},
                {pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true},

                {pattern: 'karma-test-shim.js', included: true, watched: true},

                // paths loaded via module imports
                {pattern: 'app/*.js', included: false, watched: true},
                {pattern: 'test/*.js', included: false, watched: true},

                // paths to support debugging with source maps in dev tools
                {pattern: 'app/*.js.map', included: false, watched: false},
                {pattern: 'test/*.js.map', included: false, watched: false},
                {pattern: 'app/*.ts', included: false, watched: false},
                {pattern: 'test/*.ts', included: false, watched: false}
            ]
        });
};

Die Konfiguration legt fest, welche Assertion Library verwendet werden soll (jasmine), auf welchem Port der Karma HTTP Server lauschen soll, das Loglevel sowie den Browser, der zur Testausführung verwendet werden soll. Details zur Konfiguration finden sich in der Karma-Dokumentation: http://karma-runner.github.io/0.13/config/configuration-file.html.

Durch files wird konfiguriert, welche Dateien benötigt werden, um die Tests bzw. die Anwendung auszuführen. Im Beispiel werden alle bekannten Angular- und sonstigen Abhängigkeiten zur Ausführung aufgeführt. Zusätzlich wird die Datei karma-test-shim.js eingeführt – später mehr dazu. Die (kompilierte) Anwendung und Test-JS-Dateien werden zunächst nur geladen und nicht direkt ausgeführt, dies erledigt System.js zu einem späteren Zeitpunkt. Da alle Anwendungs- und Testdateien durch den Browser ausgeführt werden und die meisten Browser sowie Karma selbst nur ES5 unterstützen, werden nur die Transpilate ausgeführt. Damit beim Debugging (zum Beispiel in den Chrome Dev Tools) der richtige Quellcode verlinkt ist und nicht nur das ES5 Transpilat, werden schlussendlich noch die TypeScript-Quelldateien und zugehörige *.map-Dateien inkludiert.

Die Datei karma-test-shim.js dient dazu, im Rahmen von Karma den System.js Module Loader von Angular 2 bereitzustellen und im Lifecycle zu integrieren: Das Shim unterbricht den normalen, synchronen Start der Karma-Testausführung und stößt ihn zu einem späteren Zeitpunkt erneut an, wenn alle Anwendungs- und Testdateien geladen sind und für die Ausführung zur Verfügung stehen. Das karma-test-shim ist auf GitHub verfügbar und auch Teil des Beispielprojekts von diesem Artikel.

Analog zum vorhergehenden Artikel sieht die erweiterte Dateistruktur nun folgendermaßen aus:

projectRoot/        - index.html
                        - package.json
                        - tsconfig.json
                        - typings.json
                        - karma.conf.js
                        - karma-test-shim.js
                        - app/
                           - boot.ts
                            - simple.component.ts
                        - test/ 
                                - simple.test.ts
                        - node_modules/
                        - typings/

In der Datei simple.test.ts befindet sich ein einfacher Unit-Test für die Komponente, die in TypeScript dann folgendermaßen aussieht:

import {it, describe, expect, beforeEach} from 'angular2/testing';

import {SimpleComponent} from "../app/simple.component";
describe('Simple Component Test', () => {

        let component: SimpleComponent;
        beforeEach(() => {
                    component = new SimpleComponent();
        });

        it('should be defined', () => {
                    expect(component).toBeDefined();
        });

        it('should be an implementation of "Component"', () => {
                    expect(component).toBeAnInstanceOf(SimpleComponent);
        });
});

In der ersten Zeile werden die im Test verwendeten Funktionen importiert. Auffallend ist, dass der Import von angular2/testing stammt und nicht von Jasmine. Das liegt daran, dass Angular 2 die vorhandene Jasmine-Funktionalität, mit eigenen Erweiterungen versehen, bereitstellt. Danach wird die zu testende Komponente, SimpleComponent, importiert.

Tests werden in sogenannten Testsuites zusammengefasst. Eine Testsuite wird durch die describe-Funktion begonnen, die ein Stringargument als beschreibenden Namen erwartet und als Rumpf die einzelnen Tests enthält. Im Beispiel werden Lambda-Ausdrücke verwendet, um eine kompaktere Schreibweise für die inneren Funktionen zu haben.

Um innerhalb der Tests auf eine Instanz der Komponente zugreifen zu können, wird eine Referenz unter dem Namen component mittels let deklariert. Die beforeEach-Funktion wird durch Jasmine vor jedem einzelnen Test ausgeführt und erzeugt eine neue Instanz der SimpleComponent. Jeder Testfall (auch „spec“ genannt) wird durch die it-Funktion gekapselt. Auch diese Funktion erwartet einen String-Parameter um den Testfall zu beschreiben. Durch diese Syntax unterstützt Jasmine, dass die Tests sich – zumindest auf Englisch – relativ natürlich lesen lassen: Der erste Test aus dem Beispiel wird „It should be defined“ (engl. „Sie soll definiert sein“) gelesen. In dem Test wird geprüft, ob der Variable ein Wert zugewiesen ist. Da durch das beforeEach immer eine neue Instanz erzeugt wird, sollte der Test auch immer wahr sein. Das zweite Beispiel aus dem Test stellt sicher, dass das Objekt auch vom Typ SimpleComponent ist.

Auch die Properties der Objekte können überprüft werden. Im folgenden Beispiel wird geprüft, dass der initiale Wert des nameProperty gesetzt ist:

describe('Component initialization Test', () => {
        let component: SimpleComponent;
        beforeEach(() => {
                    component = new SimpleComponent();
        });

        it('should have an initial value of ”here”', () => {
                    expect(component.name).toEqual('here');
        });
});

In umfangreicheren Testsuites kann auch die Interaktion der Komponente mit dem Browser-DOM getestet werden. Beispiele dafür sind Initialisierung des DOMs, Reaktion auf Events wie Knopfdruck oder Messages von (Mock-)Services können implementiert werden. Einige weitergehende Beispiele finden sich im begleitenden Beispielprojekt auf GitHub.

Integration in NetBeans

Damit NetBeans Unterstützung für die Testausführung bieten kann, muss Karma konfiguriert werden. Dies geschieht in NetBeans im eigenen Konfigurationspunkt Karma unter Optionen („Tools“ -> „Options“, dann der Reiter „HTML/JS“ und der Unterpunkt „Karma“). Um den Pfad, wo sich Karma befindet, zu konfigurieren wird durch „Browse…“ und dem sich anschließend öffnenden Filebrowser die Position des global installierten Karma-cli ausgewählt. (Die globale Installation sollte vorher durch den Kommandozeilenbefehl npm install --global karma-cli vorgenommen worden sein.)
 
image01
 
Wichtig: Hier ist die globale Version von Karma-cli auszuwählen, nicht Karma selbst. Würde man anstatt Karma-cli nämlich Karma nehmen, müssten auch globale Installationen von verschiedenen Karma-Plug-ins, wie z.B. Karma-Jasmine, vorgenommen werden. Karma-cli nutzt im Gegensatz dazu die im jeweiligen Projekt lokal installierte Karma Version, wie sie in der package.json spezifiziert ist.

Sehr komfortabel ist der Modus von Karma, um auf Dateiänderungen zu lauschen und dadurch die Ausführung aller Tests zu triggern. Dies Verhalten kann in NetBeans in den Projekteinstellungen („Settings“ -> „JavaScript Testing“) vorgenommen werden. Alternativ dazu kann durch einen Rechtsklick auf „Karma“ und dort „Properties“ die Konfiguration vorgenommen werden. Dies ist im folgenden Screenshot zu sehen.
 
image02
 
Im Eigenschaften Dialog kann dann durch den Eintrag „Watch for file changes and rerun tests automatically“ das Verhalten aktiviert bzw. deaktiviert werden.
 
image03
 
Nachdem die grundlegenden Einstellungen vorgenommen wurden, gibt es verschiedene Möglichkeiten, Karma zu starten:

Zum einen steht das npm-Script „test“ zur Verfügung, welches sogar sogar ohne die NetBeans Integration von Karma arbeitet. Dies kann durch Rechtsklick auf das Projekt, dann „npm Scripts“ und Auswahl von „test“ geschehen. Um von der vollen Integration in NetBeans zu profitieren, wählt man nach einem Rechtsklick auf das Projekt im sich öffnenden Popupmenü „Test“ aus. Als Tastaturkürzel kann dies auch über „Alt + F6“ erfolgen.
 
image06
 
In beiden Fällen wird der Browser Chrome gestartet, um als Laufzeitumgebung für die Tests zur Verfügung zu stehen. Die Anzeige im Browser ist unspektakulär und sieht wie im folgenden Screenshot aus:
 
image09
 
Wenn der Karma Support in NetBeans verwendet wird, wird das Ergebnis der Tests im NetBeans-üblichen Ausgabedialog für Tests angezeigt, wie im folgenden Screenshot zu sehen:
 
image04
 
Wenn alle Tests erfolgreich durchlaufen sind, ist ein grüner Balken zu sehen. Sind Tests fehlgeschlagen, so färbt sich der Balken rot und die fehlgeschlagenen Tests sind im unteren Teil des Fensters zu sehen.

Wird lediglich das npm-Script verwendet, so erscheint lediglich die Textausgabe im Konsolenfenster, was der Ausgabe aus dem rechten Teil des Ausgabedialogs für Tests (s. oben) entspricht.

Zusammenfassung

Test-driven development ist mit Angular 2, TypeScript und IDE-Support möglich. Karma und Jasmine erlauben kontinuierliche Testausführungen und können damit während der gesamten Projektlaufzeit für mehr Sicherheit und Qualität sorgen.

Unterstützung bei Entwicklung, Frontend-Architektur oder Schulungen rund um Angular 2 und Webentwicklung werden u.a. vom Autor durch die trion GmbH angeboten.

Das Beispielprojekt mit Quelltexten zur Demonstration von Angular 2 Tests mit Karma und Jasmine befindet sich auf GitHub: https://github.com/codecoster/2016-angular-simple.

Aufmacherbild: Traffic lights with the green light lit. von Shutterstock / Urheberrecht: Marijus Auruskevicius

Geschrieben von
Karsten Sitterberg
Karsten Sitterberg
Karsten Sitterberg ist als freiberuflicher Entwickler, Trainer und Berater für Java und Webtechnologien tätig. Karsten ist Physiker (MSc) und Oracle-zertifizierter Java Developer. Seit 2012 arbeitet er mit trion zusammen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.