• author
    • Vinzenz Haas

      Informationtechnology
    • 2. Juni 2014 in Allgemein

    CasperJS – Frontend-Testing mittels Javascript und Headless Browser

    Einleitung

    Eine globale Aussage für den Einstieg in den Post: „Jeder Betreiber einer Webpräsenz will sicherstellen dass diese Online ist und fehlerfrei funktioniert“
    Dazu gibt es die Möglichkeit des Front-End-Testings. Es werden automatisiert Webseiten und Unterseiten mittels Webbrowser aufgerufen und Teile dieser Seite überprüft. Somit kann zu einem großen Prozentsatz sichergestellt werden, dass die Webseite online ist und grundsätzlich funktioniert.

    Die meisten Softwarehersteller und Entwickler-Schmieden bieten diese Art von Tests nicht an. Bzw. wenn diese Angeboten werden, dann werden die Testfälle vom Hersteller bzw. von der Testing-Abteilung selbst erstellt.

    Bei diesem Blogpost geht es darum, selbst Testfälle aufzustellen und diese in Folge automatisiert auszuführen um sich selbst nochmals abzusichern, dass die eigene Webpräsenz einwandfrei funktioniert. Dafür haben wir die Technologie CasperJS ausgesucht.

    Technologien

    Es gibt mittlerweile mehrere Technologien die automatisiertes Frontend-Testing unterstützen. Manche davon sind benutzerfreundlich, andere eher weniger.

    CasperJS

    Da zur Zeit Javascript einen enormen Aufschwung erlebt stürzen wir uns auf CasperJS, einem Frontend-Testing Tool basierend auf PhantomJS (Headless browser basierend auf Webkit) oder SlimerJS (Headless Browser basierend auf Gecko).
    Ein „Headless Browser“ ist ein Client der ohne grafische Oberfläche ausgeführt wird. Es stellt sich die Frage warum wir diese Technolgie wählen wenn man gar nicht zusehen kann was der Browser für Vorgänge durchführt. Ein „Headless Browser“ bringt den enormen Vorteil mit, dass er so gut wie überall ausgeführt werden kann und dafür nur die Commandline benötigt wird. Im Fall von CasperJS, sollte ein Test-Case negativ ausfallen, so kann ein Screenshot der Seite gemacht werden obwohl diese nicht angezeigt wird. Das Rendering davon wird im Hintergrund übernommen.
    Der Hauptvorteil von CasperJS ist die Validierung, da diese Framework auf Javascript basiert. Somit kann mit etwas Javascript-Know-How, fortgeschrittene Test-Logik eingebaut werden.
    Wer es etwas leichter haben möchte kann ein Chrome-Plugin Ressurectio installieren und den Click-Flow aufzeichnen lassen und diesen automatisiert ausführen.
    CasperJS basiert auf MIT Licence und ist somit frei.

    Selenium

    Selenium ist zur Zeit eines der am weitest verbreitetsten Testumgebungen für Webanwendungen. selenium-logo
    Mittel Selenium IDE (Firefox Plugin) werden Pfade durchs Web aufgezeichnet und als Art Makro gespeichert, welches gleich darauf ausgeführt werden kann.
    Diese Tests können über Schnittstellen in verschiednene Browsern ausgeführt werden. Weiters gibt es auch noch die Möglichkeit mittels Selenium Grid dieses Tests verteilt auf Servern und in unterschiedlichen Browsern auszuführen. Damit für Softwareentwickler, DevOps und Testing-Abteilung ein Spaß dabei ist können die Tests in Continuous Integration angehängt werden. Die Technolgie basiert auf der Apache-Lizenz und ist somit frei zum Download.

    Ranorex

    Da Ranorex ein proprietäres Tool ist, werden bedeutend mehr Features angeboten um unterschiedliche Internet-Technolgien zu testen.
    Der Vorteil beim Kauf von Ranorex ist der Support und die Dokumentation zur Implementierung von den verschiedensten Frontend-Tests auf verschiedensten Endgeräte, wie z.B. PC in verschiedensten Browser, Mobiletelefone (Android, IPhone, …) und Tablets.
    Um diese Features genauer anzuführen müsste man einen eigenen Post schreiben. Ihr könnt euch selbst ein Bild davon machen unter diesem Link.

    Test-Cases in CasperJS

    Vorraussetzungen zur Ausführung von CasperJS

    Es muss „nodejs“ und dem dazugehörigen „npm“ (Node Package Manager) installiert sein. Zur Installation gibt es hier zum Donwload und dazugehörige Anleitungen http://nodejs.org/download/
    https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager

    Mittels „npm“ werden die Tools phantomjs und casperjs heruntergeladen und automatisch installiert.

    npm install -g phantomjs
    npm install -g casperjs
    

    Dadurch sind alle Vorraussetzungen, um Tests in CasperJS zu schreiben, gegeben.

    Das erste Beispiel ist ein einfacher Testfall in dem überprüft wird ob unser Blog http://www.schmidee.com online ist.
    Den unten angeführten Code in einer Datei abspeichern (in unserem fall wurde die Datei „check_schmidee.js“ genannt).

    Schmidee Online

    casper.test.begin('Überprüfung ob schmidee.com online ist', function(test) {
        casper.start();
    
        casper.thenOpen("http://www.schmidee.com/", function(response) {
            test.assertEquals(response.status, 200, "http://www.schmidee.com/");
        });
    
        casper.run(function() {
            test.done();
        });
    });
    

    Der Test kann nun über die Commandline ausgeführt werden.

    casperjs test check_schmidee.js
    

    Der erste Test ist fertig und wurde erfolgreich ausgeführt.
    Das Ergebnis davon (sollte unser Blog online sein ;)) schaut in etwa wie folgt aus.

    casperjs_result_200_schmidee

    XXXLutz – Test der Produktsuch- und Warenkorb-Funktionalität

    In diesem Testfall wird die Funkionalität von CasperJS mehr ausgereizt.
    Darunter fallen unterschiedliche Validerungen, Ajax Request, Interaktion mit der Webseite wie Formulare befüllen, …

    Als Basis wurde der Deutsche Möbel-Shop von XXXLutz genommen. In Textform niedergeschrieben werden folgende Schritte durchgeführt.

    • Besuch der Seite http://www.xxxlshop.de
    • Leere Produktsuche
    • Das erste Produkt im Listing wird besucht
    • Die Produktanzahl wird um 1 erhöht
    • Das Produkt wird in den Warenkorb gelegt
    • Es wird eine inkorrekte Deutsche Postleitzahl angegeben um die Postleitzahl-Validierung der Seite zu überprüfen
    • Es wird eine korrekte Deutsche Postleitzahl angegeben

    Durch diesen Click-Flow wird ein ein Teil des Checkout-Prozesses der Seite abgedeckt.

    In Code-Form schaut das so aus. Den Code in der Datei xxxlutz.de.js abspeichern.

    function thenWithWait(cb, waitTime) {
        var wait = waitTime || 300;
        casper.then(cb);
        casper.wait(wait);
    }
    
    function thenOpenWithWait(url, waitTime) {
        var wait = waitTime || 300;
        casper.thenOpen(url);
        casper.wait(wait);
    }
    
    casper.test.begin('xxxlutz.de checkout postalcode', function(test) {
        casper.start('http://www.xxxlshop.de');
    
        casper.options.viewportSize = {width: '800px', height: '600px'};
    
        thenWithWait(function() {
            casper.waitForSelector('form[class="headerSearchForm"]');
            test.assertTitle('XXXL - Ihr Einrichtungshaus');
            test.assertExists('form[class="headerSearchForm"]');
        });
    
        thenWithWait(function() {
            this.fill('form[class="headerSearchForm"]', {}, true);
        });
    
        thenWithWait(function() {
            test.assertTitle('XXXL Suche');
            test.assertExists('#productsHeader h1');
        });
    
        thenWithWait(function() {
            var h1 = this.evaluate(function() {
                return __utils__.findOne('#productsHeader h1');
            });
            //TODO test regex productcount
            this.evaluate(function() {
                this.echo(__utils__.findAll('h1').textContent);
            });
        });
    
        thenWithWait(function() {
            test.assertEval(function() {
                return __utils__.findAll('article.productCell').length > 10;
            }, 'amount for productcells on empty search is greater than 10');
        });
    
        thenWithWait(function() {
            this.echo('Navigating to first product from listing');
            casper.thenOpen(this.evaluate(function() {
                return __utils__.findOne('article.productCell a.goToDetails').href;
            }));
        });
    
        thenWithWait(function() {
            this.echo('Showing productdetailpage of product ' + this.getTitle());
            this.echo('clicking spinner button up');
            this.click('.spinner a.btn.up');
        });
    
        thenWithWait(function() {
            this.echo("Clicking spinner increase quantity");
            test.assertEval(function() {
                return __utils__.findOne('.productQuantity').value == 2;
            }, 'product quantity is 2');
            this.echo("Adding product to cart");
            this.click('#add2cart button[type="submit"]');
        });
    
        thenOpenWithWait('https://www.xxxlshop.de/checkout');
    
        thenWithWait(function() {
            test.assertTitle('Mein Warenkorb');
            test.assertEval(function() {
                return __utils__.findAll('article.cartentry').length == 1;
            }, 'Cart contains one product');
    
            test.assertEval(function() {
                return __utils__.findOne('input.quantity').value == 2;
            }, 'Quantity of product is 2 in cart');
        });
    
        thenWithWait(function() {
            this.fill('#cartform', {postalcode: 99999}, true);
            test.assertEval(function() {
                return __utils__.findOne('#postalcode').value === '99999';
            }, 'postalcode is set to 99999');
            this.wait(2000);
        });
    
        thenWithWait(function() {
            test.assertEval(function() {
                return __utils__.findOne('input.quantity').value == 2;
            }, 'Quantity of product is still 2');
    
            test.assertEvalEquals(function() {
                return __utils__.findOne('input.quantity').value;
            }, '2', 'Quantity of product is still 2 assertEvalEquals');
        });
    
        thenWithWait(function() {
            test.assertVisible('div.field-error.errorpostalcode');
        });
    
        thenWithWait(function() {
            this.fill('#cartform', {postalcode:  10115}, true);
            test.assertEval(function() {
                return __utils__.findOne('#postalcode').value === '10115';
            }, 'postalcode is set to 10115');
            this.wait(2000);
        });
    
        thenWithWait(function() {
            test.assertNotVisible('div.field-error.errorpostalcode');
        });
    
        casper.run(function() {
            test.done();
        });
    });
    
    

    Da auf manchen Seiten SSL verwendet wird muss CasperJS die Option „–ignore-ssl-errors=true“ angehängt werden, dadurch wird das SSL-Zertifikat nicht überprüft.

    Zum ausführen des Tests schaut der komplette Command so aus

    casperjs test xxxlutz.de.js --ignore-ssl-errors=true
    

    Das Ergebnis davon in etwa so.

    casperjs_xxxlutz_de_result

    Die Dokumentation für CasperJS ist hier zu finden.

     

    Ausblick

    Mit dieser Technologie ist es recht einfach neue Testcases zu erstellen, abzulegen und mittels Cronjob auszuführen. So kann eine Test-Suite für die gesamte Funktionalität der Seite gebaut werden.

    Viel Spaß beim ausprobieren!

    Quellen:
    http://casperjs.org/images/casperjs-logo.png
    http://docs.seleniumhq.org/images/big-logo.png