Die wenigsten Webseiten kommen heutzutage ohne Interaktivität aus. Dabei steigt der Umfang und die Komplexität der erwarteten Funktionalitäten immer weiter an. Da zur Umsetzung häufig JavaScript verwendet wird, kann man es sich auch hier nicht mehr leisten, auf automatisierte Tests zu verzichten.

JavaScript ist in den letzten Jahren ein wesentlicher Bestandteil der Entwicklung von Webanwendungen geworden. Die Zeiten, als JavaScript nur für einfache Effekte und nervende Werbepopups verwendet wurde, sind lange vorbei. Stabile und einfach zu benutzende Frameworks erlauben es, immer mehr Logik vom Server auf den Client zu verlagern.

Leider hat JavaScript, beim gemeinen Java Entwickler, häufig immer noch einen schlechten Ruf. Mit der Idee, in einer Anwendung die Clientseite komplett in JavaScript umzusetzen, erntet man schnell ein überzeugtes „Pfui“. Ein wesentlicher Punkt für dieses „Pfui“ ist die Testbarkeit. In der Vergangenheit, war diese für JavaScript auch tatsächlich kein Thema gewesen. Ein Test bestand darin, die Seite aufzurufen und zu hoffen, dass alles funktionierte.

Durch Zunahme der Komplexität von Anwendungen – und spätestens bei der Entwicklung umfangreicher Frameworks (z.B. jQuery) – ist dieser Ansatz nun nicht mehr ausreichend. Daher ist es nur natürlich, dass mittlerweile auch zahlreiche Testframeworks existieren, die für jeden Zweck und Geschmack etwas bieten. Eine Übersicht der verfügbaren Frameworks gibt es hier.

Marketing & Sales Lösungen - Ihr Wettbewerbsvorteil
Erfolgreiche und effiziente Aktivitäten in den Bereichen Marketing & Sales benötigen vor allem Planung, Monitoring und passende Supportlösungen.

Der einfache Start

Für mein letztes Webprojekt wollte ich die Client Oberfläche mit Angular JS umsetzen. Da meine Erfahrungen, sowohl mit Angular JS als auch mit Tests in JavaScript, doch sehr übersichtlich ausfielen, musste ich erst einmal klein anfangen.

Zuerst musste das passende Testframework gefunden werden. Diese Entscheidung war letztendlich sehr einfach, da Angular JS sehr gut mit Jasmine zusammenarbeitet. Jasmine ist klar und einfach strukturiert (Testfall-Notation des Behavior Driven Development (BDD)) und seine Dokumentation ist einfach zu lesen und mit vielen Beispielen ausgestattet. Also genau das richtige, für mich als Anfänger. Ich habe mit der Version 1.3 von Jasmine gearbeitet, mittlerweile gibt es aber auch eine Version 2.0.

Mein erster Test war schnell geschrieben. Eine simple Funktion mit einem sehr einfachen Testfall:

Die Funktion

var basic={};
  basic.addValues = function ( a, b ) {
  return a + b;
};

Der Test

describe("basic jasmine", function() {
  describe("addValues(a, b) function", function() {
    it("should equal 3", function(){
      expect( basic.addValues(1, 2) ).toBe( 3 );
    });
    it("should equal 3.75", function(){
      expect( basic.addValues(1.75, 2) ).toBe( 3.75 );
    });
    it("should NOT equal '3' as a String", function(){
      expect( basic.addValues(1, 2) ).not.toBe( "3" );
    });
  });
})

Der Testcode beschreibt hier 3 Test, die jeweils mit it beginnen. Über describe lassen sich Tests zu Gruppen zusammenfassen. Diese Gruppen können dann, mit einem weiteren describe, erneut zusammengefasst werden.

Wichtig werden diese Gruppierungen dann, wenn nicht alle Tests ausgeführt werden sollen, sondern nur eine Teilmenge. Schreibt man ddescribe, werden nur die Gruppen mit ddescribe ausgeführt und Gruppen mit describe ignoriert. Will man nur einzelne Tests ausführen, so können diese mit iit beschrieben werden. Andersherum lassen sich mit xdescribe bzw. xit Gruppen und einzelne Test deaktivieren. Ein häfiges Problem ist es, dass ein iit schnell vergessen werden kann, wodurch alle anderen Tests nicht ausgeführt werden.

Für eine weitere Erklärung der verschiedenen Matcher und Spies möchte ich auf die Dokumentation der jeweiligen Jasmine Version verweisen.

Nun haben wir zwar einen Test geschrieben, aber wie können wir diesen ausführen? Jasmine benötigt dafür einen sogenannten SpecRunner. Dabei handelt es sich grundlegend um eine normale HTML-Seite. In dieser werden die Jasmine Bibliotheken, die JavaScript-Dateien und die spec-Dateien eingebunden. Bei spec-Dateien handelt es sich um JavaScript-Dateien, welche die Tests enthalten, die den suffix .spec.js im Namen tragen. Dadurch können JavaScript-Dateien, welche Produktivcode enthalten, von den Dateien mit Tests unterscheiden werden. Idealerweise existiert zu jeder Datei mit Produktivcode, eine spec-Datei (z.B. basic.js und basic.spec.js).

Der SpecRunner

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>                                                         

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>           

  <script type="text/javascript" src="src/basic.js"></script>
  <script type="text/javascript" src="src/basic.spec.js"></script>                           

  <script type="text/javascript">
    /* jasmine init */
    (function() {
      var jasmineEnv = jasmine.getEnv();
      jasmineEnv.updateInterval = 1000;                                                      

      var htmlReporter = new jasmine.HtmlReporter();                                         

      jasmineEnv.addReporter(htmlReporter);                                                  

      jasmineEnv.specFilter = function(spec) {
        return htmlReporter.specFilter(spec);
      };                                                                                     

      var currentWindowOnload = window.onload;                                               

      window.onload = function() {
        if (currentWindowOnload) {
          currentWindowOnload();
        }
        execJasmine();
      };                                                                                     

      function execJasmine() {
        jasmineEnv.execute();
      }                                                                                      

    })();                                                                                    

  </script>
</head>
<body>
</body>
</html>

Der SpecRunner führt die Tests automatisch aus, wenn die Seite im Browser geöffnet wird.

Wir finden individuelle Lösungen
Planungs- und Entscheidungsprozesse in Unternehmen basieren auf zahlreichen Daten und deren Gewichtung. Wir finden individuelle Lösungen, die Ihren speziellen Anforderungen entsprechen und binden bestehende Systeme optimal ein.

Was ist mit dem DOM?

Unser bisheriges Testszenario kann nur pures JavaScript testen. In realen Projekten wird oft aber auch der DOM-Baum manipuliert. Diese Anwendungsfälle werden, von Jasmine selbst, nicht direkt unterstützt. Doch mit jasmine-jquery steht eine fertige Lösung bereit. Es erweitert Jasmine um zusätzliche Matcher und eine API, um mit HTML, CSS und JSON-Fixtures umzugehen. Mit diesen Erweiterungen lassen sich Tests für reale HTML Seiten schreiben, welche mittels der Fixtures geladen werden. Auf die Elemente dieser geladenen Seite, kann dann mit normalen jQuery Selectoren zugegriffen werden.

Hier ein Beispiel, bei dem eine test.html als Fixture geladen wird. Danach wird geprüft, ob bestimmte Elemente in der Seite existieren bzw. verschiedene Events ausgelöst werden. Abschließend wird das Ergebnis geprüft.

describe("jQuery Tests", function() {
    var spyEvent;                                                                      

    beforeEach(function() {
        jasmine.getFixtures().fixturesPath = 'src';
        jasmine.getStyleFixtures().fixturesPath = 'src';
        loadFixtures('test.html');
        loadStyleFixtures('test.css');
    });                                                                                

    it("should load html", function() {
        expect($('#test-form')).toContainHtml('<h1>Test Form</h1>');
    });                                                                                

    describe("Button Click Event Tests", function() {
        var spyEvent;                                                                  

        beforeEach(function() {
            testJquery().init();
        });                                                                            

        it ("should invoke the btnShowMessage click event.", function() {
            spyEvent = spyOnEvent('#btnShowMessage', 'click');
            $('#btnShowMessage').trigger( "click" );                                   

            expect('click').toHaveBeenTriggeredOn('#btnShowMessage');
            expect(spyEvent).toHaveBeenTriggered();
            expect($("#pMsg")).toHaveText('show me');
        });                                                                            

        it ("should invoke the btnHideMessage click event.", function() {
            spyEvent = spyOnEvent('#btnHideMessage', 'click');
            $('#btnHideMessage').trigger( "click" );                                   

            expect('click').toHaveBeenTriggeredOn('#btnHideMessage');
            expect(spyEvent).toHaveBeenTriggered();
        });
    });
});

 

Karma

Jetzt können wir bereits Tests für reines JavaScript, DOM-Manipulationen und Events erstellen und ausführen.

Leider ist die Durchführung der Tests alles andere als optimal. Den SpecRunner im Browser menuell immer wieder aufzurufen macht nur bedingt Spaß. Besonders dann, wenn die Tests auf verschiedenen Browsern ausgeführt werden sollen. Wir benötigen also einen neuen Testrunner, der sich am Besten auch direkt in unsere Entwicklungsumgebung integriert.

Auf der Suche nach einem Testrunner für JavaScript stößt man schnell auf Karma. Im wesentlichen handelt es sich dabei um ein Programm, welches erst einen Webserver startet und dann, auf allen konfigurierten Browsern, den Produktivcode gegen den Testcode ausführt. Die Installation von Karma ist etwas aufwändiger als unser bisheriger einfacher SpecRunner. Als erstes benötigen wir Node.js, denn Karma ist ein Node.js-Paket. Nachdem wir dies heruntergeladen und installiert haben, kann Karma mit folgendem Befehl auf der Kommandozeile installiert werden:

npm install karma --save-dev

Zusätzlich benötigen wir noch das Jasmine Plugin, damit Karma unsere Tests interpretieren kann.

npm install karma-jasmine --save-dev

Um die Tests auf verschiedenen Browsern auszuführen, müssen zudem noch Launcher Plugins installiert werden.

Beispielsweise für Chrome:

karma-chrome-launcher --save-dev

Während der Entwicklung hat sich die Nutzung von Phantom JS bewährt. Dabei wird kein kompletter Browser, samt grafischer Oberfläche, gestartet, wodurch die Ausführung der Test sehr schnell erfolgt.

npm install -g phantomjs
npm install karma-phantomjs-launcher –save-dev

Nachdem wir nun Karma und die notwendigen Plugins installiert haben, muss noch eine Konfiguration erstellt werden. Üblicherweise wird diese karma.conf.js benannt.


// Karma configuration
// Generated on Mon Jul 21 2014 12:46:37 GMT+0200 (Mitteleuropäische Sommerzeit)

module.exports = function(config) {
config.set({

// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',

// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],

// list of files / patterns to load in the browser
files: [
{pattern: 'lib/jquery.js', watched: false, served: true, included: true},
{pattern: 'lib/jasmine-jquery.1.7.0.js', watched: false, served: true, included: true},
{pattern: 'src/*.js', watched: true, served: true, included: true},
{pattern: 'src/*.html', watched: true, served: true, included: false},
{pattern: 'src/*.css', watched: true, served: true, included: false}
],

// list of files to exclude
exclude: [

],

// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {

},

// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],

// web server port
port: 9876,

// enable / disable colors in the output (reporters and logs)
colors: true,

// level of logging
logLevel: config.LOG_INFO,

// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,

// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Firefox','PhantomJS'],

// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false
});
};

Für eine genaue Beschreibung der einzelnen Parameter der Konfiguration empfiehlt sich die Dokumentation auf der Karma Seite.

Gestartet wird Karma auf der Kommandozeile mit folgendem Befehl:

karma start karma.conf.js

Sämtliche komfigurierte Tests (im Parameter files) werden jetzt auf allen angegebenen Browsern (aus dem Parameter browsers) ausgeführt. Da der Parameter autoWatch auf true gesetzt wurde, überwacht Karma die Dateien auch auf Änderungen. Wenn Änderungen festgestellt werden, so werden die Test automatisch erneut ausgeführt.

Neben dem Start von Karma auf der Kommandozeile gibt es auch eine Integration in die IDEs von JetBrains, wie z.B. WebStorm und IntelliJ.

BPM-Software - so individuell wie Sie
Unternehmen können sich nur auf ihre Herausforderungen konzentrieren, wenn die Geschäftsprozesse reibungslos funktionieren. Individuelle Softwarelösungen tragen an dieser Stelle zum erfolgreichen Management bei.

Build Prozess

Nachdem wir die Tests für alle gewünschten Browser automatisiert ausführen können, fehlt nur noch der letzte Schritt: Die Integration in den Build Prozess. In meinem Fall bedeutet das eine Integration in Maven. Dafür kann das Maven-Karma-Plugin verwendet werden. Die Konfiguration des Plugins sieht folgendermaßen aus:

<plugin>
    <groupId>com.kelveden</groupId>
    <artifactId>maven-karma-plugin</artifactId>
    <version>1.6</version>
    <executions>
        <execution>
            <phase>test</phase>
            <goals>
                <goal>start</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <configFile>src/test/config/karma.conf.js</configFile>
        <karmaExecutable>${karma.path}</karmaExecutable>
        <browsers>PhantomJS</browsers>
        <karmaFailureIgnore>false</karmaFailureIgnore>
    </configuration>
</plugin>

Zu beachten ist hier der Parameter karmaExecutable. Da die Entwicklung auf Windows erfolgt, die Build Nodes aber auf Linux laufen, muss hier jeweils der passende Pfad für die Windows- und Linux-Version von Node.js verwendet werden. Das kann z.B. über Maven Profiles erreicht werden, welche den korrekten Pfad einstellen, je nach verwendetem Betriebssystem.

<profile>
    <id>Windows</id>
	<activation>
	    <os>
			<family>Windows</family>
		</os>
	</activation>
	<properties>
	    <karma.path>
	        ${project.build.directory}/nodejs/NodeWin/node_modules/.bin/karma
	    </karma.path>
		<karma.broswer>
		    PhantomJS
		</karma.broswer>
		<nodejs.os>
		    win
		</nodejs.os>
	</properties>
</profile>
<profile>
	<id>Linux</id>
	<activation>
		<os>
			<family>Linux</family>
		</os>
	</activation>
	<properties>
	    <karma.path>
	        ${project.build.directory}/nodejs/NodeLinux/node_modules/.bin/karma
        </karma.path>
		<karma.broswer>
		    PhantomJS
	    </karma.broswer>
		<nodejs.os>
		    linux
	    </nodejs.os>
	</properties>
</profile>

Damit steht dem Testen von JavaScript Code prinzipiell nichts mehr im Wege. Was fehlt sind noch Statistiken zur Messung der Testabdeckung und Code-Qualität. Doch auch hier gibt es bereits Lösungen. Zur Testabdeckung kann der geneigte Leser z.B. das Saga-Maven-Plugin genauer unter die Lupe nehmen und für die Code-Qualität das JSLint-Maven-Plugin.

Diskutieren Sie mit!

Es steht Ihnen frei einen Kommentar zu hinterlassen. Sie finden Informationen zur Verwendung Ihrer Daten in unserer Datenschutzerklärung.

Alle mit einem markierten Felder sind Pflichtfelder und müssen ausgefüllt werden