Wie schon ein paar mal geschrieben, bin ich schwer begeistert wie einfach es mit angularJS ist, Web Apps mit Javascript zu entwickeln. Vielleicht merkt man das auch an der ein oder anderen Stelle 🙂
Aber egal, wie einfach das Entwickeln ist, wir machen alle Fehler. Deshalb muss auch angularJS-Code getestet werden, mindestens Unit-Tests besser noch mit End-2-End-Tests.
[highlight1]Aber moment mal, Javascript-Code und Unit-Tests? Geht das überhaupt? [/highlight1]
Aber klar! Schon lang gibt es diverse Test-Frameworks für Javascript. jQuery benutzt z.B. qunit, die AngularJS-Entwickler bevorzugen Jasmine.
Als Skriptsprache, die beim Kunden im Browser ausgeführt wird, ist es bei Javascript-Tests natürlich wichtig, sie dort auszuführen wo sie laufen: im Browser. Und das ist ohne spezialisierte Tools äußerst aufwändig.
Als sogenannter Test Runner, gibt es seit einigen Jahren jsTestDriver (von Google), das Unit-Tests entgegennimmt und dafür sorgt, sie auf angeschlossenen Browsern auszuführen. Leider habe ich keine gute Erfahrung damit gemacht, da es allgemein recht instabil läuft, mehrfach täglich neugestartet werden muss und nach Neustart das automatische Wiederverbinden mit den Browsern eher hakelig ist. Außerdem scheint die Weiterentwicklung auch eher eingeschlafen zu sein.
Die AngularJS-Entwickler haben sich deshalb entschlossen, einen eigenen Test Runner zu entwickeln. [highlight1]Testacular[/highlight1].
Testacular ist kein Testframework, sondern wirklich nur der Test Runner. Man schreibt die Unit-Tests also z.B. als Jasmine-Tests und führt sie mit Testacular in Chrome oder Firefox oder Safari oder auch in PhantomJS (Headless Browser) aus.
Da wir AngularJS u.a. auch mit requireJS benutzen, ist es allerdings nicht ganz einfach, als Anfänger zu verstehen, wie man Testacular konfigurieren muss, damit requireJS den Produktions- und Testcode asynchron nachladen kann und erst danach Testacular loslegt um die Tests auszuführen.
Standardmässig liefert Testacular nämlich eine Webseite an den Browser aus, in der alle referenzierten *.js-Dateien automatisch als <script>-Tag referenziert werden, damit der Browser sie lädt. Nach Laden der Seite fängt dann Testacular an, die Tests auszuführen. Mit requireJS darf Testacular allerdings die Skripte eben gerade nicht mittels <script>-Tag laden, sondern dies übernimmt require.js. Nur die main.js um requireJS zu laden und zu konfigurieren, darf so eingebunden werden.
Als Startpunkt für meine Konfiguration mit requireJS habe ich einen Artikel bei Jake Trent gefunden, der sich genau diesem Problem widmet. Im Gegensatz zu diesem Artikel kann ich aber sagen, dass die komplette requireJS-Konfiguration wie man sie im Produktionsbetrieb gewohnt ist, auch für Testacular funktioniert. Testacular bindet mit meiner Konfiguration nur noch Jasmine, requireJS und die main-test.js ein. Alles andere lädt requireJS nach.
Dies erreicht man mit folgender Konfiguration der Dateien:
files = [
JASMINE,
JASMINE_ADAPTER,
REQUIRE,
REQUIRE_ADAPTER,
{pattern:'src/main/external-libs/angular-1.0.3.js', included:false},
{pattern:'src/test/external-libs/angular-mocks-1.0.3.js', included:false},
{pattern:'src/main/js/**/*', included:false}, //Produktionscode
{pattern:'src/test/unit/**/*.js', included:false}, //Testcode
'src/test/main-test.js' //requireJS-Konfiguration
];
Entscheidend ist hier das [hightlight1]included:false[/highlight], was dafür sorgt, dass die Dateien zwar verfügbar sind, aber nicht per <script> automatisch ausgeliefert werden. Der Rest der Konfiguration kann so aus dem verlinkten Artikel übernommen werden.
In der main-test.js Datei muss man nun dafür sorgen, dass die eigene Anwendung geladen wird (die ja sowieso schon aus require-Module bestehen sollte), die Testklassen geladen werden (das müssen nun auch alles require-Module sein, siehe Artikel) und wenn das alles geladen ist, muss Testacular mitgeteilt werden, dass es nun loslegen kann. Meine main-test.js sieht wie folgt aus:
(function (window, require) {
"use strict";
var file, requireModules;
requireModules = [];
//aus den von Testaular bereitgestellten Dateien alle *Test-Dateien raussuchen und Pfad anpassen, damit requireJS
//was damit anfangen kann
for (file in window.__testacular__.files) {
if (window.__testacular__.files.hasOwnProperty(file)) {
if (file.substring(file.length - 7, file.length) === 'Test.js') {
requireModules.push("../.." + file.substring(9, file.length - 3));
}
}
}
// unsere eigene Anwendung inkl. Abhängigkeiten laden
requireModules.push("app");
//angular Mocks laden
requireModules.push("mocks");
require({
// "/base" ist die URL von der Testacular alle Dateien ausliefert,
// "src/main/js" ist due Base-URL aller Module die nicht Test sind
baseUrl:'/base/src/main/js',
paths:{
'angular':'../external-libs/angular-1.0.3',
'mocks':'../../test/external-libs/angular-mocks-1.0.3'
},
shim:{
'angular':{ exports:'angular' },
'mocks':{ deps:['angular'], exports:'mocks' }
}
}, requireModules, function () {
//erst hier wird Testacular tatsächlich (manuell) gestartet
window.__testacular__.start();
}, function (err) {
//hier kommt die Fehlerbehandlung hin
});
}(window, require));
Über den Zugriff auf [highlight1]window.__testacular__.files[/highlight1] hole ich mir alle Testdateien und bastele daraus die korrekt benamten require-Module. Dann wird noch die eigentliche Anwendung und Angular hinzugefügt. Wenn requireJS alles geladen hat, wird dann über [highlight1]window.__testacular__.start();[/highlight1] Testacular gestartet.
Wenn man das einmal begriffen hat, ist es eigentlich recht einfach. Aber das ist ja häufig so 🙂 Testacular ohne requireJS zu konfigurieren, ist auch deutlich einfacher.
Wenn es dann einmal läuft, hat man aber einen hervorragenden Testrunner, den man dann auch in Webstorm (Javascript IDE) integrieren kann, um direkt in der IDE, die Unit Tests starten zu können und – besonders beeindruckend – auch direkt in der IDE debuggen kann.
Aber dazu ein anderes mal mehr Details.