Türchen 06: Frontend-Tests mit CasperJS

Normalerweise kommt am 6. Advent der Nikolaus, nicht aber bei uns, heute kommt nämlich der Kasper. Und nicht irgendeiner, sondern der CasperJS! Bevor ich jedoch erkläre, was dieses CasperJS ist, möchte ich euch gerne einen Überblick über den folgenden Beitrag geben.

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.

V-Modell

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. 2Testpyramide
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
  1. http://martinfowler.com/bliki/TestPyramid.html 
  2. http://www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid 


Ein Beitrag von Don Bosco Nguyen van Hoi
Don's avatar

Don Bosco van Hoi ist studierter Wirtschaftsinformatiker und hat über 10 Jahre Erfahrung in der professionellen Softwareentwicklung. Anfang 2013 gründete er mit Andreas Emer die Mothership GmbH als E-Commerce Agentur, die auch regelmäßig den Münchener Stammtisch veranstaltet. Im Umfeld professioneller Softwareentwicklung gelten seine Interessen vor Allem den Open Source Projekten Zend-Framework und AngularJS. Neben der Softwareentwicklung interessiert er sich für die Herausforderungen im agilen Projektumfeld, insbesondere der Vertragsgestaltung von agilen Projekten.

Alle Beiträge von Don

Kommentare
Webguys Magento Adventskalender | Mag-tutorials.de am

[…] Türchen 06: Frontend-Tests mit CasperJS […]

Don Bosco Nguyen van Hoi am

Hallo Rokko,

danke für dein Feedback. Selenium wird in der Tat oftmals oft mit der Selenium IDE in Verbindung gebracht. Das sehe ich auch anders und wird im Artikel nicht wirklich hervorgehoben. Unter Selenium verstehe ich heutzutage hauptsächlich einen Grid, der in der Lage ist, mehrere Hubs(einzelne Browser) durch das Webdriver-Protokoll zu bedienen. Im Alltag hat es sich aber auch etabliert, Selenium Tests mit der IDE aufzunehmen, diese zu exportieren und dann serverseitig ausführen zu lassen.

Das meinte ich mit den Rumgeklicke im Frontend. Dabei möchte ich Selenium keineswegs schlecht reden. Selenium und nicht die Selenium IDE ist ein großartiges Tool, um auf verschiedene echte Browser zu testen. Ganz tolle Ansätze verfolgt zum Beispiel auch codeception (http://www.codeception.com) oder nativ auch die Facebook Webdriver-API.

Überhaupt ist das Webdriver-Protokoll eine großartige Sache. Das ist bei CasperJS zum Beispiel auch ein Nachteil, da es lediglich ein Superset von PhantomJS darstellt und PhantomJS nicht über das Webdriver-Protokoll bedient. Ich denke jedoch, dass dies hier zu vernachlässigen ist, da es vor Allem eine technologisch interessante Spielwiese ist, sich jedoch auch für das ernsthafte Testing eignet.

Wer jedoch auf Selenium testen möchte, sollte sich alle Lösungen anschauen, die wie gesagt, das Webdriver-Protokoll implementieren.

Also noch mal herzlichen Dank für dein konstruktives Feedback.

Don Bosco

PS. Unser Nikolaus war da :)

Rokko am

Hallo Don Bosco,

erstmal wünsche ich einen schönen Nikolaustag. Bei mir war leider nichts drinnen im Schuh :( Aber immerhin ein sehr interessanter und hilfreicher Beitrag im Adventskalender!

Ich fand es super interessant zu sehen, dass es neben dem erwähnten Selenium-Framework auch Javascript-Basierte Lösungen gibt.

Jedoch muss ich widersprechen: Das von dir zitierte (und leider auch relativ schlecht gewertete) Selenium wird synonym für das Selenium-IDE-Browser-Plugin verwendet. Ich möchte an dieser Stelle darauf aufmerksam machen, dass das natürlich nur Selenium verwendet.

Ich stimme vollkommen überein, dass ein pixelgenaues Geklicke nicht als Test zu bezeichnen ist, da ein Test schlecht wartbar und vor allem nicht von langer Dauer ist. Das Selektieren von CSS-Elementen nach id, name, class, tag, ... und sämtliche von dir beschriebenen Vorteile beherrscht Selenium natürlich auch.

Hierzu habe ich einen netten Beitrag zum Erstellen von Selenium-Tests mit Scala gefunden: Die Tests lesen sich leicht runter und ist somit auch lesbar und verständlich für nicht-Programmierer.

http://ebeab.com/2011/05/28/functional-testing-with-selenium-webdriver-and-scala/

Vielen Dank auch für das coole Github-Repo! Eine schöne Vorweihnachtszeit!

Ciao! Rokko

Dein Kommentar