Javascript testen mit QUnit und Sinon.JS
Gastartikel von Markus Frühauf.
Ich bin 25 Jahre und seit über 7 Jahren im Webbereich tätig, davon 3 Jahre Perl und 4 Jahre jQuery, jQueryUI und php, Anfangs mit dem Framework von phpBB und seit zwei Jahren mit dem Zend-Framework (in mehreren Projekten / Firmen). Ich entwickle Webanwendungen von der Datenbank bis zum Frontend.
Vor kurzem war ich auf der IPC11 in Mainz, dort hatte ich mir den Beitrag Javascript Testing angeschaut, dieser war extrem interessant für mich, da ich schon länger auf der Suche bin nach Unittesting für jQuery und Javascript war.
Dort hab ich ganz klein nebenbei das Mocking Framework Sinon.JS auf geschnappt. Dies war sozusagen das letzte Puzzelstück für meine Unittests, was ich gesucht hab. Sinon.JS kann Mocking, Spyes, Stubs und noch mehr. Dann hat PHPGangsta in seinem Linkpool Sinon.JS verlinkt, er hat es auch auf der IPC11 auf geschnappt.
Mittlerweile hab ich schon die ersten Gehversuche mit Sinon.JS gemacht, aber das Mocking wollte nur mit Requests funktionieren welche per POST abgeschickt wurden. Das war nur ein halber Sieg, da Daten laden per GET nicht funktionieren wollte. Zu dem Problem mit GET komme ich etwas weiter unten im Artikel.
Da ich regelmäßig den Blog von Michael lese hab ich ihn bezüglich Sinon.JS angeschrieben. Von ihm hab ich dann die Antwort bekommen, er hat mit Frontend nicht viel am Hut und kann mir nicht weiter helfen, aber ich könnte einen Blog Artikel schreiben, welchen er für mich veröffentlicht. Ok die Idee war gut, aber ich hatte mittlerweile den Ehrgeiz es selber herauszufinden wieso GET nicht will.
Google war mir in diesem Fall kein so guter Freund, aber ich hatte Ehrgeiz und mit etwas Hirn hab ich das Problem gelöst. Aber die Idee darüber einen Blogartikel zu schreiben war mir immer noch im Kopf, also hab ich gesagt, ich schreib einen, vielleicht bekomme ich somit ein paar neue Ideen.
Die Lösung für mein Problem war:
Ich hatte $.Ajax so konfiguriert das es, wenn es Daten per GET vom Server holt, diese nirgendwo gecacht werden.
$.ajaxSetup({ cache: false });
Bei Sinon.JS muss man die komplette URL angeben oder mit Regex anpassen, diese hat in meinem Fall zwar immer gepasst, aber ich hatte vergessen, dass jQuery bei Cache: false einen Timestamp als GET Parameter anhängt. Während eines QUnit-Tests ist dieser Timestamp immer 0.
So genug geschrieben jetzt zum Code: http://jsfiddle.net/RMrSt/13/
Ich hab den Code sehr schön dokumentiert, damit jeder sich dort zurecht findet.
- TestModul-A: Ist ein QUnit-Test um einen einfachen Taschenrechner zu testen, dort wird Stub von Sinon.JS verwendet um zu Prüfen ob bei einer Division durch 0 ein Alert erzeugt wurde.
- TestModul-B: Geht davon aus das ein Backend das Ergebnis als JSON Object zurückgibt.
Und das schöne ist man kann die QUnit-Tests relativ einfach in Jenkins einbinden.
Ich hab dazu PhantomJS verwendet. Eine sehr gute Anleitung um PhantomJS unter Jenkins einzurichten ist in diesem noch relative neuem Blogbeitrag beschrieben.
Ein ganz großes Problem das ich dort entdeckt hab und noch keine Lösung habe: PhantomJS kann nur ein Success oder Failed zurückgeben. Dies macht es dann sehr mühsam den fehlgeschlagenen Test zu finden.
Ich hab bei uns im Jenkins Server die Javatestes noch um jslint4Java erweitert, dieses sucht Codefehler so wie es phpLint für PHP macht.
Jetzt möchte ich den Jenkins noch mit dem Closure Compiler erweitern, welcher Javascript Code schön klein macht und damit die Ladezeiten verringert werden.
Fazit:
- Es ist relativ leicht ein QUnit-Test zu machen, da QUnit die Syntax von jQuery verwendet.
- Wenn einmal ein Test geschrieben ist, kann man an der Funktion bauen und man kann sich sicher sein das die Funktion noch wie vorher funktioniert
Noch ein paar Fragen an euch Leser:
- Testet überhaupt wer sein Javascript?
- Wie lasst ihr automatisiert euer Javascript Testen?
Alternatives Mocking Tool für jQuery / Javascript ist jQuery-Mockjax.
Alternatives Modul für Jenkins um QUnit Tests anzubinden wäre der JsTestDriver, mit welchem ich noch keine Erfahrungen gesammelt hab.
Ich hoffe euch hat dieser kleine (mit etwas mehr Code) Beitrag gefallen.
Markus Frühauf
Meine Test Bemühungen für Javascript Sachen sind zugegeben eher Anfänger Niveau. Für eine Javascript Klasse bzw. für eine Aufgabe schreibe ich try-Dateien. z.B. try.overlay.php. Dort baue ich dann die Klassen ein die ich brauche, inklusive einiger Tests. Wenn man die Seite aufruft, kann man meist allerhand Buttons klicken bzw. sieht das Verhalten meistens sofort.
Das hat jedoch ein paar Vorteile. Zum einen kann man Grafische Effekte testen (das ist mit einem Unittest eher schwer).
Zum anderen hat man eine kleine Dokumentation wie die Sachen ein zu bauen sind. Objekte und deren Methoden werden ja benutzt.
Nachteil ist auf jeden Fall, dass es nicht automatisiert abläuft.
T-Rex
7 Nov 11 at 10:51
Ich teste, aber benutze Jasmine (). Mit QUnit bin ich nie wirklich klargekommen (ich hab’s versucht), aber die API ist grauenvoll:
equal(a, b); // Is a the expected value or b?
Ich kann mir die Reihenfolge schlecht merken, weil sie einfach nur Vereinbarungssache ist. Das kann man auch sehr schön daran sehen, dass es bei PHPUnit genau umgekehrt ist:
QUnit: equal( actual, expected, [message] )
PHPUnit: assertEquals(mixed $expected, mixed $actual[, string $message = “])
Es ist aber auch „sprachlich“ hässlich. „equal(foo, bar);“ – was?
In Jasmine:
expect(foo).toEqual(bar);
*Damit* kann ich doch was anfangen! Liest sich auch flüssig, finde ich.
Timo Reitz
7 Nov 11 at 15:34
Statische Codeanalyse wird bei unserem sehr großem ExtJS-Projekt mit JSLint erledigt — ebenfalls mit dem bereits erwähnten jslint4java in einem mittels Ant gewrappten Maventask. Damit sind diverse Fehler im Grunde bereits ausgeschlossen, etwa der Klassiker „Trailing Comma“ im IE. Der ist bei uns sogar so scharf eingestellt, dass dieser Fehler etwa den gesamten Buildprozess hart mit einem Error im Jenkins abbricht. Hurt your feelings.
Im ersten Schritt wird nach der Kompilierungsphase (ist Java, also vor dem Bauen der WAR) jede Datei durch die leichte Stufe des Google Closure Compiler gejagt (WHITESPACE ONLY). Das ist leider noch so, weil ansonsten leider die ExtJS Komponenten nicht mehr korrekt funktionieren (Stand Anfang des Jahres mit ExtJS3).
Zur Laufzeit (hier: beim ersten Aufruf über’s Web) werden dann im zweiten Schritt die bis zu 400 JS-Dateien in 2-3 Dateien gepackt und statisch im Space abgelegt (bis zum nächsten Restart des Servers). Zu diesem Zeitpunkt sind die Daten dann auch komplett minifiziert.
Und leider ja: Tatsächlich haben wir sehr, sehr wenige automatische Tests (im Kontrast zu der Unmenge an Komponenten). Das lässt sich aber insoweit relativieren, als das mit der Verwendung vorgefertigter Komponenten die Seiteneffekte im Verhältnis zur Menge im überschaubaren Rahmen bleiben. Außerdem — das sei hier erwähnt — ist der überwiegende Teil eine gleichartige Lesedarstellung (wenn auch in vielfältiger Hinsicht); die eigentliche Arbeit, Prüfung und Verteilung der Datenoperationen geschieht selbstverständlich serverseitig.
Für mich hat sich mittlerweile herausgestellt, das eine global orientierte Strukturierung bei der Größenordnung extrem wichtig ist (hier: komponentenorientiertes Design, dank ExtJS). Lieber ein paar Zeilen „überflüssigen“ Code durch gleich aussehende Strukturen als kompakte, nicht mehr wartbare Master-Funktionsobjekt-Generator-Funktionen. Strg+Shift+F (bzw. Cmd+Shift+F) hat sich in Eclipse bewährt: Damit wird das Autoformat angestoßen (was man m.E. eh immer anhaben sollte): Das funktioniert nur, wenn es keinen Parseerror gibt.
—
Falls jemand praktische Erfahrung von automatischen Tests bzw. Testverfahren von kompletten RIAs (ExtJS) gemacht hat, da wäre ich gerne an einem Informationsaustausch interessiert.*
* Wer jetzt meint, „Das kann doch gar nicht so schwer sein!“:
1) In ExtJS haben Komponenten eine technisch gesehen zufällige ID, d.h. ich muss Selektoren (wie etwa bei Selenium) immer manuell so bauen, dass sie in verschiedenen Situationen immer noch passen. Für eine Story muss ich also den Weg exakt nachbauen. Darüber hinaus kann bereits das Verschieben einer Komponente das HTML-Gerüst ändern (was zwar keine funktionielle oder funktionale Änderung mit sich führt, aber jeden Test nach Murphy gegen die Wand fahren lässt). Schlussendlich muss man sich die Frage stellen, ob der Aufwand eines automatischen Komplett-Tests dem Aufwand gerecht wird. Die dynamischen, großen im Browser laufenden, automatisch ausgeführten Tests (via Selenium) sind extrem zeitaufwendig und bringen in einem agilen, sich ständig veränderndem Produkt nur dann etwas, wenn sie stetig angepasst werden (klar soweit). Basisfunktionen kann man abdecken, aber eine komplett Abdeckung ist überhaupt nicht notwendig.
2) Die Testbarkeit einzelner Komponente (also das Unit-Level) ist z.T. genau dann fragwürdig, wenn man bspw. eine Tabelle nur mit Konfigurationsdaten füllt. Dabei reden wir nicht von 20 Tabellen, sondern eher von 200+ verschiedenen Darstellungen solcher; es ist nicht immer (sowohl zeitlich als auch logisch) sichergestellt, dass die Datenquellen gleichartig strukturiert sind. Man könnte sich die Mühe machen, und die Konfiguration der Geschäftslogik nachtesten; denn ein Testen der eigentlichen Funktionalitäten ist bereits durch das Framework geschehen und würde zu „doppelten“ Tests führen.
3) Verbleiben Core- und Util-Tests: Die können perfekt getestet werden und brauchen zudem i.d.R. kein DOM-Enviroment. In unserem Fall sind dies hauptsächlich zentrale Mini-Utils, etwa zur Darstellung der lokalen Zeit oder dem Rendern spezieller Stati.
Jan
7 Nov 11 at 20:56
Nachdem ich so viel geschrieben habe, fällt mir allerdings eine wichtige Ergänzung ein: Meine o.g. Punkte Punkte für IMHO unpraktische Testbarkeit bezieht sich hauptsächlich auf die zahlreichen View-Objekte.
Wäre auch hier interessant zu wissen, wie das andere machen: Testet man nur „Non-Views“ und keine „Views“? Oder alles — wenn ja, wie?
Jan
8 Nov 11 at 07:44
[…] Autor, Markus Frühauf, des Gastartikels auf phpgangsta.de, hat dort ziemlich ausführlich erklärt, wie man mit Hilfe von Sinon.JS und QUnit seine […]
Javascript Unittesting mit QUnit und Sinon.js at voodoo4u/NET
25 Dez 11 at 23:28