Suche
Testing und Code-Qualität

Nashorn-JavaScript mit Spock und Jasmine testen

Niko Köbler

Unser Kolumnist Niko Köbler greift diesmal eine etwas exotischere Testvariante auf: Nashorn-Javascript mit Spock und Jasmine testen. Und weil das gut funktioniert, ist „geht nicht“ keine Ausrede mehr, Nashorn-JavaScript nicht zu testen.

Vor circa eineinhalb Jahren, kurz nachdem Java 8 mit der neuen Nashorn JavaScript Engine [1] veröffentlicht wurde, sprach mich nach einem Vortrag über Node.js auf der Java Virtual Machine ein Zuhörer an, wie es sich denn mit dem Testen von Nashorn JavaScripts verhält. Ihm gefiele Nashorn und die sich daraus ergebenden Möglichkeiten sehr gut, aber er weiß noch nicht, wie er seinen eigenen JavaScript Code testen könne. Da ich mich ehrlich gesagt bis zu dem Zeitpunkt auch noch nicht viel mit dem Thema JavaScript-Tests mit Nashorn beschäftigt hatte, begann ich mit diesem Zuhörer im Gespräch einen ersten Ansatz zu entwickeln. Und da es bis heute meiner Meinung nach noch zu wenig Blog-Posts oder Websites zu dem Thema gibt, wollte ich dieses Thema endlich einmal schriftlich festhalten. Alle Beispiele aus diesem Artikel sind auch in meinem GitHub Repository [2] zu finden.

Einfache Tests mit Spock

Grundsätzlich gibt es zwei einfach Möglichkeiten, auf Berechnungsergebnisse in Nashorn zuzugreifen:

  1. Der eval-Befehl der ScriptEngine gibt direkt den Wert zurück, den die Ausführung des Ausdruckes ergeben hat.
  2. Über ScriptEngine.get kann auf Variablen im Engine-Kontext zugegriffen und deren Werte damit ausgelesen werden.

Diese Werte können dann mit einem Assert auf die gewünschten Ergebnisse überprüft werden. Damit sähe ein JUnit-Test in etwa so aus:

    @Test
    public void testNashornJavaScript() throws ScriptException {
      ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
      Object result = nashorn.eval("1 + 1");
      Assert.assertEquals(2, result);
    }

Die Unterhaltung war fruchtbar, mein Gesprächspartner hatte damit einen ersten Praxisansatz und ging fohen Mutes nach Hause; ich zunächst auch. Jedoch beschäftigte mich das Thema weiter und mit einem einfachen Unit-Test wollte ich mich nicht zufrieden geben. Da ich in letzter Zeit viel lieber mit Spock [3] unter Groovy als mit JUnit unter Java teste, schrieb ich den Test des Abends erst mal nach Spock um:

    def "simple Nashorn JavaScript test"() {
      given:
      def nashorn = new 
ScriptEngineManager().getEngineByName('nashorn');

      when:
      def result = nashorn.eval('1 + 1')

      then:
      result == 2
    }

Wenn man es genau nimmt, sind es erst mal drei Zeilen mehr Code, aber der Vorteil, der sich durch die Power-Assertions (direkte Vergleiche zweier Werte) und zusätzlichen Features im weiteren Verlauf erschließt, ist einfach unbestechlich. Wir werden das noch sehen. Die ausführliche Spock-Dokumentation ist hier zu finden.

In der Praxis ist es jedoch meist so, dass der JavaScript Code in separaten .js-Dateien abgelegt ist. Diesen Code möchte ich nun auch in meinen Spock-Tests überprüfen. Bleiben wir also beim Beispiel des Taschenrechners und erzeugen uns eine Datei calculator.js:

    var add = function(a, b) {
      return a + b;
    }

    var square = function(a) {
      return a * a;
    }

Diesen Taschenrechner-Code laden wir zunächst, zu Beginn des Tests über die Methode setup, in Nashorn. Die ScriptEngine wird diesmal als „gemeinsame“ Klassen-Variable mit der Annotation @Shared initialisiert.

    @Shared ScriptEngine nashorn = new 
ScriptEngineManager().getEngineByName('nashorn')
    def setup() {
      nashorn.eval(this.class.getResource('/calculator.js').text)
    }

Der erste Test, um die Additions-Logik zu testen, ist weiterhin unspektakulär:

    def "test add"() {
      when:
      def result = nashorn.invokeFunction('add', 1, 2)

      then:
      result == 3
    }

Ähnlich wie in unserem ersten Spock-Beispiel, überprüfen wir hier einfach das Ergebnis aus 1+2, was 3 ergeben muss. Allerdings rufen wir die Addition nicht mehr direkt auf, sondern rufen jetzt die Funktion add im Nashorn-Kontext auf und übergeben dieser die Parameter 1 (für a) und 2 (für b). Da wir calculator.js vorher in den Engine-Kontext geladen haben und die Funktion in der Datei als globale Funktion definiert ist, steht uns die add()-Funktion global in Nashorn zur Verfügung.

Nun möchten wir aber testen, ob das Verhalten unseres Codes auch mit unterschiedlichen Werten noch stimmt. Unter JUnit mussten dafür immer mehrere Tests herhalten, Copy and Paste war angesagt, massenhaft Code-Duplikate, etc. Spock kann hier Daten-getrieben (data-driven) arbeiten, sodass der eigentliche Testcode nur ein einziges Mal geschrieben und dann mit unterschiedlichen Werten immer wieder neu aufgerufen werden kann. Für die Definition der Werte kommt zu den bekannten given-, when-, then-Blöcken noch ein where-Block hinzu:

    @Unroll
    def "square of #a is #expected"() {
      when:
      def result = nashorn.invokeFunction('square', a)

      then:
      result == expected

      where:
      a | expected
      1 | 1
      2 | 4
      3 | 9
      4 | 16
    }

Die erste Zeile im where-Block definiert die Variablen, die im vorherigen Test-Code verwendet werden, hier a und expected. Spock führt nun für jede Datenzeile den Test mit allen in der Zeile angegebenen Werten für die Variablen aus. Die Annotation @Unroll sorgt dafür, dass für jede Testausführung auch eine Instanz an den Test-Runner zurückgeliefert wird, sodass dort für jede Ausführung ein Ergebnis eingetragen wird. Damit kann man genau erkennen, welcher Test mit welchen Daten fehlgeschlagen ist oder erfolgreich war.

Da Spock auf Groovy basiert und Groovy von Haus aus Mocking-Klassen mitbringt, können mit diesen auch einfach Funktionen gemockt werden, die man nicht direkt testen kann oder will: Zum Beispiel Callback-Funktionen oder die in Nashorn nicht vorhandene alert()-Methode, die es nur im Browser gibt. Beispiele hierzu sind in meinem GitHub-Repository unter CallbackSpec zu finden.

Das Ganze mit Jasmine

Das war bis hierher noch recht einfach. Im JavaScript-Ökosystem haben sich jedoch andere Test-Frameworks als JUnit oder Spock durchgesetzt, z. B. Mocha, Karma oder das wahrscheinlich am weitesten verbreitete und bekannteste Test-Framework Jasmine [4] für vorrangig Behaviour-Driven-Tests, die zu alledem noch im Browser ausgeführt werden.

Kann man Jasmine-Tests mit Spock-Tests zusammenbringen?

Spoiler: Die nachfolgenden Schilderungen sind nich wirklich ernst zu nehmen und nicht für einen produktiven Einsatz bestimmt, sondern einfach aus der „weil-es-geht“-Laune heraus entstanden!

Die Test-Beschreibung mit Jasmine sieht für unseren Taschenrechner wie folgt aus:

    require('./calculator.js');

    describe("suite 1", function() {
      it("should pass", function() {
        expect(add(1, 2)).toBe(3);
      });
    });

Sehr leserlich wird beschrieben, dass unsere „Suite 1“ erfolgreich sein sollte („should pass“) und wir erwarten („expect“), dass die Addition von 1 und 2 hier 3 („toBe“) ergibt. Um diese Jasmine-Testsuite nun unter Nashorn ausführen zu können, ist noch etwas „Glue-Code“ notwendig, um verschiedenen Dinge zueinander zu bringen, die so erst mal nicht für einander gemacht sind.

Grundsätzlich werden Jasmine-Tests eigentlich im Browser ausgeführt. Nashorn ist aber kein Browser und hat damit auch keine Window- und DOM-spezifischen Funktionen, diese müssen wir über Polyfill nachbauen bzw. mocken, damit die Jasmine-Bibliothek nicht auf Fehler läuft. Dies habe ich mit jasmine-html-polyfill.js getan. Nun kann in einer jasmine-bootstrap.js-Datei Jasmine selbst in Nashorn geladen werden:

    var require = function(path) {
      load(Java.type("dasniko.spock.nashorn.JasmineSpec").class.getResource(path).toExternalForm());
    };

    var window = this;

    require("/jasmine-core/lib/jasmine-core/jasmine.js");
    require("/jasmine-html-polyfill.js");
    require("/jasmine-core/lib/jasmine-core/boot.js");
    load({script: __jasmineSpec__, name: __jasmineSpecName__});

    onload();

    jsApiReporter.specs();

Die require()-Funktion aus CommonJS bauen wir uns für unsere Bedürfnisse nach, da Nashorn kein CommonJS kennt. Wir müssen für unseren Test lediglich die Ressourcen mit dem gleichen Classloader laden, mit dem auch die letztendliche Testklasse (JasmineSpec, s.u.) gestartet wird. Die Funktion load() ist Teil der Nashorn API und ermöglicht es uns, Dateien aus dem Dateisystem zu laden. Aus den umfangreichen Jasmine-Bibliotheken benötigen wir lediglich die Core-Libs jasmine.js und boot.js, ergänzt um unsere Polyfill-Datei. Jasmine habe ich über den Node Package Manager npm dem Projekt hinzugefügt und den Ordner node_modules, in dem NPM die JS-Bibliotheken speichert, als zusätzlichen Projekt-Classpath deklariert. Das build.gradle findet sich hier.

Zeile 10 aus dem obigen Script lädt dann unsere eigentliche Test-Suite jasmine-testsuite.js, onload(). In Zeile 12 startet die Test-Ausführung und mit Zeile 14 können wir uns die Test-Ergebnisse aus der Jasmine-API als JSON zurückgeben lassen. Diese Ergebnisse importieren wir dann in die Spock-Tests und können sie somit übersichtlich mit all unseren anderen Testergebnissen gemeinsam darstellen.

Die Klasse JasmineSpec.java halten wir zunächst etwas generisch (und abstract), um verschiedene Jasmine-Testsuites damit ausführen zu können. In der setupSpec-Methode befüllen wir also ein paar globale Script-Variablen mit einigen Meta-Informationen, die mit der implementierenden Klasse geliefert werden, übergeben diese an Nashorn, laden unsere jasmine-bootstrap.js-Datei und starten damit auch implizit die Jasmine-Testauführung:

    @Shared def jasmineResults

    def setupSpec() {
      def scriptParams = [
        "__jasmineSpec__"    : getMetaClass().getMetaProperty("SPEC").getProperty(null),
        "__jasmineSpecName__": "${this.class.simpleName}.groovy"
      ]

      nashorn.getBindings(ScriptContext.ENGINE_SCOPE).putAll(scriptParams);
      jasmineResults = loadJS('/jasmine-bootstrap.js')
    }

Die Testergebnisse liegen nun also in der Variablen jasmineResults vor, diese müssen wir nun nur noch in Spock importieren. Dies geschieht mithilfe einer Spock-Testmethode, in der wir über die Ergebnisse in jasmineResults iterieren und mit der Hilfsmethoden isPassed() specErrorMsg() den jeweiligen Status als Spock-Ergebnis zurückliefern (die vollständige Implementierung findet sich hier):

    @Unroll
    def "#specName"() {
      expect:
      assert isPassed(item), specErrorMsg(item)

      where:
      item << jasmineResults.collect {it.value}
      specName = (item.status != 'pending' ? item.fullName : "IGNORED: $item.fullName")
    }

Diese Implementierung von JasmineSpec, ist als JasmineCalculatorSpec lediglich ein Einzeiler (ok, drei Zeilen):

    class JasmineCalculatorSpec extends JasmineSpec {
      static def SPEC = this.class.getResource('/jasmine-testsuite.js').text
    }

Führt man die Tests nun aus, erhalten wir einen Test-Report wie diesen, auf dem wir wie gewünscht sehen können, welche Jasmine-Tests erfolgreich waren und welche fehlgeschlagen sind:

Jasmine Test-Ergebnisse in Spock (© Niko Köbler)

Jasmine Test-Ergebnisse in Spock (© Niko Köbler)

Fazit

Es ist also recht einfach möglich, die selbstgeschriebenen Nashorn-JavaScripte im Kontext von JUnit oder Spock mit zu testen. Ich rate auch jedem, das auch wirklich zu tun. „Geht nicht“ gibt’s hier nicht. Sogar die Einbeziehung eines JavaScript Testrunners und der Import der Ergebnisse in Spock ist möglich, wie wir gesehen haben. Das Prinzip, das wir hier bei Jasmine gesehen haben, ist ähnlich mit Mockito und anderen Frameworks umsetzbar. Ob man dieses Vorgehen allerdings im produktiven Einsatz durchführen möchte, sei jedem selbst überlassen. Interessant ist es allemal!

Spannend ist vielleicht auch noch das Testen von JavaScript Templating-Engines. So habe ich in dem oben genannten Beispielprojekt auch eine HandlebarsSpec, die den korrekten Aufbau von Handlebars Templates unter Nashorn sicher stellt.

In diesem Sinne – fröhliches Testen!

Geschrieben von
Niko Köbler
Niko Köbler
Niko Köbler ist freiberuflicher Software-Architekt, Developer & Trainer für Java & JavaScript-(Enterprise-)Lösungen, Integrationen und Webdevelopment. Er ist Co-Lead der JUG Darmstadt, schreibt Artikel für Fachmagazine und ist regelmäßig als Sprecher auf internationalen Fachkonferenzen anzutreffen. Niko twittert @dasniko.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.