Vorwort
Heute beschäftigen wir uns mit Testautomatisierung im Bereich Frontend. Zum jetzigen Zeitpunkt existieren etliche Tools, um Web-Oberflächen zu testen. Den meisten Lesern hier dürfte Selenium bekannt sein. Vor allem findet Selenium im Bereich der End-to-End Tests Verwendung. Ein typischer Workflow ist die Erstellung der Tests anhand eines Makros und die manuelle erneute Ausführung der Tests, sobald ein Entwickler den Code geändert hat. Natürlich gibt es hier auch die Möglichkeit, die Tests im Rahmen einer Continues-Integration-Strategie zu portieren und zu automatisieren. Dieses Thema würde jedoch den Rahmen dieses Artikels sprengen.
Obwohl diese Tests sehr einfach, sogar mit wenigen, oder gar keinen Programmierkentnissen erstellt und ausgeführt werden können, sind diese Tests sehr oft der Grund dafür, dass sich Entwickler beklagen. Das hat viele Ursachen:
- Der Browser ist langsam. Einfache Tests sind durchaus hilfreich, komplette Testsuites können jedoch bei mehreren Entwickler die Entwicklungszeit drastisch erhöhen.
- Oberflächentests sind sehr anfällig für Veränderungen. Eine Veränderung am System kann einen ganzen Rattenschwanz an weiteren Veränderungen nachziehen. Das ist zwar auch bei Unit-Tests der Fall, jedoch sind Unit-Tests durch ihre Eigenschaften (Komponententests, lose Kopplung usw.) weniger anfällig für Seiteneffekte.
- Oberflächentests sind nicht deterministisch. In der Informatik liefern deterministische Abläufe bei fest definierten Zuständen stets die gleiche Ausgabe. Wenn die Datenbank zum Beispiel hängt, oder andere abhängige externe Systeme nicht funktionieren wie sie sollten, so ist dieses Verhalten nur schwer kontrollierbar.
Viele Entwickler stellen sich also die Frage, welcher Lösungsansatz gewählt werden soll. Jedoch existieren so viele unterschiedliche Testframeworks, so dass die Wahl sehr schwer fällt. Im Folgenden wird ein sehr abstrakter Lösungsansatz beschrieben.
Lösungsansatz
In der traditionellen Softwareentwicklung wird im Bereich der Qualitätssicherung oftmals auf Vorgehensmodelle wie zum Beispiel das V-Modell verwiesen. Das V-Modell kennt verschiedene Stufen der Testintegration und empfiehlt für spezifische Projektphasen entsprechende Teststrategien.
Quelle: http://www.wikipedia.de
Dieser Ansatz kann im Rahmen agiler Softwareentwicklung nicht gewählt werden, da eine phasenbasierte Entwicklung zu aufwendig wäre. Einen pragmatischeren Ansatz liefert die Test-Pyramide.1 Sie ähnelt sehr dem V-Modell, hat jedoch keinen festen Bezug zu einzelnen Projektphasen. Sie sagt jedoch aus, dass Unit-Tests das Fundament jeder Teststrategie darstellen sollten. Je höher wir die Pyramide erklimmen, um so kleiner wird die Anzahl der zu entwickelnden Tests. Damit steigt jedoch auch die Ausführungszeit der Tests. Um so höher wir also kommen, um so aufwändiger wird es, die Tests zu automatisieren und zu koordinieren. Hierzu gibt es auch einige sehr ausführliche und interessante Blogbeiträge sowie zahlreiche Literatur. 2
Quelle: http://blog.caplin.com
Wir fokussieren uns heute jedoch auf die Frontend-Tests und meiner Meinung nach müssen gute Frontend-Tests folgende Kriterien erfüllen:
- Schnelles Feedback: Wenn ein Entwickler 10 Minuten warten muss, bis ein Testergebnis bereit steht, so ist das nicht mehr effizient. Tests müssen eine kurze Laufzeit haben, um dem Entwickler zügig Feedback geben zu können.
- Geringer Adaptionsaufwand: Tests, insbesondere UI-Tests scheitern häufig und sind nicht selten bereits wenige Tage nach der Erstellung veraltet. Es muss also einfach sein, diese Tests anpassen zu können.
- Sinnvoll: Eine Komponente hat andere Qualitätsrichtlinien als eine Weboberfläche. Wenn übertrieben gesagt jeder Pixel im Frontend überprüft wird, so ist das wenig sinnvoll und auch nicht mehr produktiv.
- Spezifisch: UI-Tests dürfen nicht zu abhängig von der DOM-Struktur sein. Die Wartung von xpath oder hierarchischen CSS-Selektoren basierten Tests ist zu aufwändig. Besser ist der gezielte Einsatz von CSS-IDs oder auch CSS-Klassen.
Man könnte hier schon einen Schlussstrich ziehen und behaupten, Selenium erfülle keinen Zweck, da es fast jedem angesprochenen Punkt widerspricht. Das stimmt jedoch nicht, da das tatsächliche Testen auf verschiedenen Plattformen und Browser sehr sinnvoll ist. Man benötigt jedoch zur Entwicklungszeit eine schlankere Lösung, die diese Kriterien erfüllt: Headless Website Testing.
PhantomJS: Headless Browser
Headless Browser, was ist das eigentlich?
PhantomJS is a headless WebKit scriptable with a JavaScript API. It has fast and native support for various web standards: DOM handling, CSS selector, JSON, Canvas, and SVG.
Im Grunde genommen und vereinfacht ausgedrückt ist ein Headless Browser ein Browser ohne grafische Oberfläche. Diese Browser können daher ohne großen Aufwand auf einem Server ausgeführt werden und besitzen nur wenige Nachteile gegenüber Browser mit einer GUI, wie zum Beispiel einer fehlenden Flashunterstützung. Aber wer möchte auf dem Terminal schon Flash-Dateien testen?
Eine ohne Gewähr und auf Vollständigkeit überpfüfte, jedoch sehr umfangreiche Liste von Headless Browser und Tools findet sich bei Stackoverflow.
PhantomJS basiert auf WebKit und liefert einige nützliche Features:
- Eine umfassende API, um den Browser zu steuern
- viele Helferlein um zum Beispiel Screenshots zu erstellen, oder Third-Party Libraries einzubinden
- Eine große Community und viele interessante Projekte rund um PhantomJS
PhantomJS kann einfach direkt von den Quellen installiert werden. Eine ausführliche Installationsanleitung findet sich auf der Homepage. Nach der Installation lässt sich PhantomJS direkt von der Konsole aus starten.
me@home:~$ phantomjs --version
1.9.2
me@home:~$ phantomjs
phantomjs> console.log('Hohoho, liebe Leser!');
Hohoho, liebe Leser!
Alternativ und im Alltag die bessere Lösung ist einfach das Anlegen einer neuen JavaScript-Datei, die wir zum Beispiel hohoho.js nennen.
me@home:~$ echo "console.log('Hohoho, liebe Leser')" > hohoho.js
me@home:~$ phantomjs hohoho.js
Der aufmerksame Leser und Mitmacher merkt, dass das Skript nicht terminiert. Das liegt daran, dass der Browser explizit geschlossen werden muss. Dazu benötigt man die Funktion phantom.exit. Die gesamte API zum phantom Objekt ist nicht sehr umfangreich und kann auch in der Mittagspause schnell durchgelesen werden.
Das folgende Beispiel öffnet die Seite www.webguys.de und erstellt davon einen Screenshot.
// webguys.js
var page = require('webpage').create();
page.open('http://www.webguys.de', function() {
page.render('example.png');
phantom.exit();
});
Dass der Screenshot nicht wirklich so aussieht wie unter Chrome, Safari oder Firefox liegt unter anderem daran, dass andere Fonts benutzt werden. Das interessiert uns jedoch nicht sonderlich. Viel eher sind wir an der Struktur der Seite interessiert und dass diese ganz ordentlich aussieht, sehen wir an dem Screenshot.
Das ist zwar alles ganz schön, hat aber mit dem Testen an sich wenig zu tun.
CasperJS: Headless Browser Testing
Während BrowserJS den Browser zur Verfügung stellt, kann CasperJS als ein Test-Framework gesehen werden, das lediglich den Browser benutzt. In der Tat ist CasperJS so mächtig, dass es ohne großen Herausforderungen möglich ist, einen Crawler auf dessen Basis zu entwickeln. Der Hauptanwendungszweck ist und bleibt jedoch das Testen.
Die Installation ist ähnlich einfach wie bei PhantomJS und in wenigen Minuten erledigt. Eine detaillierte Installationsanweisung findet ihr hier. Glücklicherweise verwendet CasperJS in der Standardinstallation PhantomJS, so dass der gesamte Befehlssatz von PhantomJS zur Verfügung steht. Somit lassen sich auch alle PhantomJS-Skripte unter CasperJS ausführen.
me@home:~$ casperjs --version
1.1.0-DEV
me@home:~$ casperjs webguys.de
Nun aber mal zu einem einfachen Test. Das folgende Snippet ruft die Seite http://www.webguys.de auf und gibt einfach den Titel der Seite aus.
var casper = require('casper').create();
casper.options.verbose = true;
casper.options.logLevel = 'debug';
casper.test.begin('Homepage', function suite(test) {
// Start page
casper.start('http://www.webguys.de', function () {
this.echo(this.getTitle());
test.info('Testing Homepage');
}).run(function () {
test.done();
});
});
Output:
casperjs test homepage.js
Test file: homepage.js
# Homepage
Magento Blog für Entwickler und eCommerce-Shops - webguys.de « Magento Blog für Entwickler und eCommerce-Shops – webguys.de
Testing Homepage
Ich finde diesen Output während der Entwicklung jedoch wenig aufschlussreich, also fügen wir einfach mal zwei Zeilen dazu, die das Debugging vereinfachen.
var casper = require('casper').create();
// diese Zeilen hinzufügen
casper.options.verbose = true;
casper.options.logLevel = 'debug';
Output:
# Homepage
[info] [phantom] Starting...
[info] [phantom] Running suite: 2 steps
[debug] [phantom] opening url: http://www.webguys.de/, HTTP GET
[debug] [phantom] Navigation requested: url=http://www.webguys.de/, type=Other, willNavigate=true, isMainFrame=true
[debug] [phantom] url changed to "http://www.webguys.de/"
[debug] [phantom] Successfully injected Casper client-side utilities
[info] [phantom] Step anonymous 2/2 http://www.webguys.de/ (HTTP 200)
Magento Blog für Entwickler und eCommerce-Shops - webguys.de « Magento Blog für Entwickler und eCommerce-Shops – webguys.de
Testing Homepage
[info] [phantom] Step anonymous 2/2: done in 3967ms.
[info] [phantom] Done 2 steps in 3984ms
Assertions
Sämtliche Assertions werden in CasperJS durch den Prototypen Tester zur Verfügung gestellt. Geläufig dürften die Funktionen assert() und assertEquals() sein. Ich empfehle jedem interessierten, sich die API genauer anzuschauen und bei Bedarf einfach zu entscheiden, welche Funktionen sinnvoll sind und welche nicht. Aus meiner Erfahrung kommt man mit den einfachen assert() schon bereits sehr weit.
Wir wollen im folgenden Beispiel den vorherigen Testcase um sinnvolle Asserts erweitern. Zum Beispiel gebe ich bereits den Titel aus. Jetzt weiß ich also auch, wie der Titel lautet und kann diesen erwarteten Wert einfach überprüfen.
// Start page
casper.start('http://www.webguys.de', function () {
var title = this.getTitle();
var expectedTitle = 'Magento Blog für Entwickler und eCommerce-Shops - webguys.de « Magento Blog für Entwickler und eCommerce-Shops – webguys.de';
test.info('Testing Homepage');
test.assertTitle(expectedTitle, title);
})
Ein weiterer sinnvoller Einsatz von UI-Tests ist die Überprüfung, ob bestimmte Elemente existieren. Ich habe mir einfach mal erlaubt, den DOM zu untersuchen und mir den CSS-Selektor für das Facebook-Icon zu nehmen.
#topSocial > ul:nth-child(1) > li:nth-child(2) > a:nth-child(1) > img:nth-child(1)
Wahrscheinlich ist das nicht gerade das beste Beispiel, um ein Element zu überprüfen, da ich beim Testen von der Struktur des DOM abhängig bin. Besser wäre es, wenn ich das Element eindeutiger, zum Beispiel anhand einer ID überprüfen könnte.
Tipps und Tricks
CasperJS bietet viele nette Events. Zum Beispiel hat es sich als nützlich erwiesen, jeden einzelnen Testschritt als Screenshot aufzunehmen. So lässt sich genau nachverfolgen, an welche Stelle im Browser der Test hängt oder fehl schlägt.
Screenshots pro Testschritt aufnehmen
// On step start
casper.on("step.start", function() {
casper.capturePage();
});
// Capture the current test page
var captures_counter = 0;
casper.capturePage = function (debug_name) {
var directory = 'captures/' + casper.test.currentSuite.name;
if (captures_counter > 0) {
var previous = directory + '/step-' + (captures_counter-1) + '.jpg';
if (debug_name) {
var current = directory + '/step-' + captures_counter + '-' + debug_name + '.jpg';
} else {
var current = directory + '/step-' + captures_counter + '.jpg';
}
casper.capture(current);
// If previous is same as current (and no debug_name), remove current
if (!debug_name $$ fs.isFile(previous) $$ fs.read(current) === fs.read(previous)) {
fs.remove(current);
captures_counter--;
casper.log('Capture removed because same as previous', 'warning');
}
}
captures_counter++;
};
Setup und Teardown-Methoden benutzen
Setup und Teardown-Methoden erlauben es, Tests vor jedem Durchlauf in einen wohl definierten Zustand zu versetzen. Möglicherweise sollen immer alle Cookies gelöscht werden. Ganz einfach geht das mit folgendem Snippet:
// Set up: nothing
casper.test.setUp(function () {
// web request to reset database
});
casper.test.tearDown(function () {
// Clear cookies
casper.clearCookies();
// Reset captures counter
captures_counter = 0;
});
Strukturierung
Es empiehlt sich, die globale Testkonfiguration in einer eigenen Datei zu verwalten und Variablen zu benutzen, die als Argumente übergeben werden können. Das ist zum Beispiel sehr praktisch wenn ich wissen möchte, ob der Test auf unterschiedlichen Domains richtig durchläuft.
var url = casper.cli.get("url");
Wenn alle folgenden Test dann im Ordner /tests liegen, ist der Aufruf sehr simpel:
casperjs --pre=config.js test tests/ --url="http://my-store.com/"
Magento-Testing und der Hackathon
Im Rahmen des Magento-Hackathon in München haben wir bereits angefangen, einzelne Seiten einer Magento-Standardinstallation zu überprüfen. Der vollständiger Code befindet sich im develop-branch auf Github. Es handelt sich ausdrücklich nur um eine Machbarkeitsstudie. Dementsprechend wird das Repository nicht supportet. Ich hoffe jedoch, dass wir auf dem Hackathon einen kleinen Betrag dazu leisten konnten, die Testqualität in den Magento-Projekten zu verbessern oder zumindest einen neuen Anreiz geliefert zu haben.
Zukunft
Headless Browser Testing wird immer wichtiger. Im Rahmen von Magento-Projekten können diese Art von Tests in vielen Fällen lediglich UI-spezifische Elemente überprüfen. Der Trend geht jedoch in Richtung sehr JavaScript-lastiger Frontends. Single Page Applications, die beispielsweise mit EmberJS oder AngularJS erstellt wurden, erfordern eine performante Testinfrastruktur. Durch die Übertragung von immer mehr Anwendungslogik auf dem Client, stellen Headless Browser einen wichtigen Baustein dar.
Credits
- Jacques Bodin-Hullin für die großartige Hilfe, Inspiration und Zusammenarbeit beim Münchner Hackathon
- Den Jungs von http://www.webguys.de/ für den tollen Adventsblog