JavaScript Unit Tests sind schwer aufzusetzen? Keep calm and use Jest!

Keine Kommentare

Unit Tests sind in vielen Bereichen der Software-Entwicklung gesetzt und leisten gute Dienste. In vielen Java-Projekten zum Beispiel gehören sie zum Standard. Das Tooling ist gut, und es herrscht weitgehend Konsens unter den Entwicklern, dass Unit Tests dem Projekt helfen. Auch Test Driven Development (TDD) ist mit dieser Toolunterstützung sehr gut möglich.
Bei der Entwicklung von Frontends in Web-Projekten sind Unit Tests und TDD weniger etabliert, obwohl wir gerade im Frontend-Bereich eine stark wachsende Komplexität beobachten können: Eine zeitgemäße Web-GUI soll einer Desktop-Anwendung in nichts nachstehen.
Ein Grund aus meiner Sicht: Das Aufsetzen einer Unit-Test-Umgebung in JavaScript-Projekten ist umständlicher als zum Beispiel mit Maven oder Gradle in der Java-Welt.
Als Software Craftsman möchte in diesem Blog Post einige Impulse geben, um Unit Tests stärker im JavaScript-Umfeld zu etablieren.
Im Folgenden stelle ich mit Jest eine Testbibliothek von Facebook vor, die uns genau an dieser Stelle helfen kann. Es wurde bei Facebook im Umfeld von React entwickelt, ist aber nicht darauf beschränkt. Ich gebe eine kleine Einführung in den Funktionsumfang von Jest und zeige, wie schnell es für ein Projekt betriebsbereit ist.

Am Anfang war der Coderetreat …

Meine erste Berührung mit Jest hatte ich nicht im Rahmen eines Kundenprojekts, sondern während eines Coderetreats. In einem Coderetreat geht es darum, in relativ kurzer Zeit eine Lösung für ein überschaubares Problem testgetrieben zu entwickeln. Man möchte hier also nicht viel Zeit in die Technik und die Infrastruktur stecken. In diesem Kontext waren für mich lange Zeit Java und Maven gesetzt, weil sie so schnell einsatzbereit sind. Mit JavaScript habe ich auch öfters experimentiert, es war aber immer viel umständlicher. Mein Vorgehen lief dann meistens auf eine Kombination von Jasmine, Karma, Babel und IntelliJ + x raus. Richtig zufriedenstellend war dies leider nie.
Ähnlich sieht dies im Kundenprojekt für viele Entwickler aus: Das Setup zum Aufsetzen von Tests für unseren Java Code ist sehr einfach. Wir können uns sehr schnell auf die Tests zu der Fachlichkeit fokussieren.
Beim Testen im Frontend fehlt oft die Zeit, die Hürden bei der Einrichtung der Umgebung zu umschiffen. Man braucht vergleichsweise lang, bis der Entwickler an den Punkt kommt, sich nur auf den Inhalt des Tests konzentrieren zu können.
Ich habe dann während eines Coderetreat einmal create-react-app ausprobiert. create-react-app ist ein Tool, das eine React-Applikation sehr schnell erzeugen (scaffolden) kann. Für ein Coderetreat viel zu viel. Aber das Tool bringt zudem eine sehr schöne Möglichkeit mit, Tests bei Codeänderung auszuführen, stellt die Testergebnisse sehr übersichtlich dar und kann ES6+ schon out-of-the-box ausführen. Super!
Das Ganze kriege ich ohne Mehraufwand auch ohne React-Abhängigkeit hin: Mit Jest!

Schauen wir uns Jest also mal genauer an:

Die Installation ist relativ unspektakulär: npm install --save-dev jest
Und folgenden Schnipsel in die package.json

{
  "scripts": {
    "test": "jest"
  }
}

Oder vielleicht sofort:

{
  "scripts": {
    "test": "jest --watch"
  }
}

Meine Tests kann ich dann per npm test starten und weise Jest per --watch an, bei Änderung von Dateien die Tests laufen zu lassen.
Jest erkennt Tests automatisch im Default daran, dass ein ‘.test’ im Namen vorkommt. Also zum Beispiel ist ein Test zu sum.js -> sum.test.js
Jest basiert auf Jasmine und erfindet somit die Testformulierung auch nicht neu. Jest-Tests sollten also für viele Entwickler gut lesbar sein.
Als Beispiel sum.test.js

const sum = require('../src/sum');
 
test('adds 1 + 2 to equal 3', () => {
 expect(sum(1, 2)).toBe(3);
});

Wir haben also bis hierhin: ES6 in Tests und einen Watchmodus, ohne etwas zu konfigurieren. Dazu noch eine Testsyntax, die – zumindest mir – immer sehr gefallen hat. Soweit so gut.
Was ich nun super finde, ist die Übersichtlichkeit, mit der Jest seine Testergebnisse (auf der Kommandozeile) darstellt.
Ändern wir mal in sum.js:

function sum(a, b) {
 return a + b +1;
}

Sehr schön! Und klappt out-of-the-box!
Fügt man nun noch den Parameter --coverage hinzu, spendiert uns Jest direkt noch eine Code-Coverage-Analyse:

Wait! There’s more: Snapshot Testing

Ein richtig tolles Features ist auch das sogenannte Snapshot Testing:
Die Idee hier ist, dass ich das Ergebnis einer Funktion aufzeichnen und dann in folgenden Testläufen den Wert des ersten Laufs mit dem Wert der folgenden Läufe vergleichen kann. Dies kann zu sehr knappem Testcode führen. Snapshot Testing ist hilfreich, falls das Aufbauen einer Vergleichsstruktur so aufwändig ist, dass es die Testintention verwässert:
Snapshot Tests kommen wie gerufen, wenn man wie in React viel Markup und Änderungen im Markup testen möchte.
Dieses Vorgehen ist aber auch praktisch, wenn man zum Beispiel Strings vergleichen möchte. Ein kleines Beispiel, um die Idee zu verdeutlichen:
Wir haben eine Funktion, die einen String zurückgibt:

exports.renderSomething = () => {
   return "My rendered String"
}

Der zugehörige Test kann wie folgt aussehen:

test('renders correctly', () => {
 expect(foo.renderSomething()).toMatchSnapshot();
});

Im ersten Testdurchlauf wird ein sogenannter Snapshot angelegt – und kein Vergleich durchgeführt. Im Hintergrund hat Jest im Testordner ein Verzeichnis __snapshots__ und eine Snapshot-Datei erzeugt. Der Inhalt sieht wie folgt aus:

exports[`renders correctly 1`] = `"My rendered String"`;

Dann ändern wir eine Kleinigkeit in unserer Funktion:

exports.renderSomething = () => {
   return "My rendered String changed"
}

Daraufhin meldet Jest uns:

Jest weißt uns darauf hin, dass sich der Wert geändert hat. Wir mussten uns nicht selbst um Testdaten kümmern. Sehr elegant!
Wenn wir nun feststellen, dass der neue Wert korrekt ist, können wir mit jest -u den Snapshot aktualisieren. Keine Angst, es gibt auch die Möglichkeit, über Pattern einzelne Snapshots zu aktualisieren. Die Snapshot-Dateien werden sinnvollerweise auch im VCS mit eingecheckt.

Dieses einfache Beispiel können wir noch ein wenig weiter denken: auch beim Testen von Legacy Code und der Verwendung eines Golden Masters extrem praktisch.

In der Praxis muss man im Team meiner Ansicht nach eine Balance zwischen Snapshot-Tests und Tests mit echt formulierten Testdaten finden, um die Wartbarkeit der Tests nicht von dieser Seite zu untergraben: Snapshot Tests sollen bewusst dort helfen, wo vielleicht sonst gar keine Tests geschrieben worden wären.

One more thing: Mocking:

Auch eine Lösung für das Mocking bringt Jest direkt mit.
Wie man das zum Beispiel von Mockito aus der Java-Welt kennt, können Mock-Funktionen erzeugt werden, die ich dann nach dem Aufruf befragen kann:

const myMock = jest.fn();
 
test('does mocking work', () => {
 bar(myMock)
 expect(myMock.mock.calls.length).toBe(1);
 expect(myMock.mock.calls[0][0]).toBe("First");
});
 
function bar(collab) {
   collab("First");
}

In diesem Beispiel testen wir, dass unser Mock genau einmal aufgerufen wurde und dass der erste Parameter “First” ist.

Man kann auch noch einen anderen Weg wählen und ganze Module wegmocken.
Nehmen wir einmal das nachfolgende Request-Modul, welches eine Funktion für eine HTTP-Anfrage bereitstellt:

function fetchData(callback) {
   let url="http://localhost:3000/hello";
   http.get(url, response => {
   let data = '';
     response.on('data', _data => data += _data);
     response.on('end', () =>  callback(data));   
   });
}

Unser Modul spricht über das Netzwerk, und das wollen wir natürlich für den Test nicht:
Wir wollen dies mit folgendem Mock ersetzen:

function fetchData(callback) {
   callback('{"hello":"world"}');
};

Wie kann das nun im Test aussehen?

Mit

jest.mock('../src/request');

weisen wir Jest nun an, unser Request Modul wegzumocken.
Jest sucht dann auf gleicher Ebene wie das Testsubjekt einen Mock.

├── __mocks__
│   └── request.js
├── foo.js
├── request.js
└── sum.js

Unter __mocks__/request.js muss dann unser Mock liegen.

Dann könnte ein Test wie folgt aussehen:

const request = require('../src/request');
jest.mock('../src/request');
 
test('async callback', (done) => {
 function callback(data) {
 
   expect(JSON.parse(data)).toEqual({"hello":"world"});
   done();
 }
request(callback);
});

Dies kann praktisch sein, allerdings finde ich die Konvention __mocks__ gewöhnungsbedürftig.

Eine Ausrede weniger

Zusammengefasst ist es schon beeindruckend, wie weit man mit Jest alleine kommt, ohne Zusatzbibliotheken nutzen zu müssen.
Darüberhinaus hat Jest noch einige Features, die speziell für React zugeschnitten sind – aber auch für ein Projekt ohne React Unterbau ist Jest ein sehr gute Wahl. Nicht nur, wenn es darum geht, möglichst schnell eine Testinfrastruktur aufzusetzen.
Eine Ausrede weniger, keine JavaScript Tests zu schreiben.
Ich hoffe euch hat dieser kleine Einblick in das Unit Testen mit Jest genauso viel Spaß gemacht wie mir! Die Codebeispiele findet ihr hier. Happy Testing!

Holger Grosse-Plankermann

Holger Grosse-Plankermann ist seit 2015 für die codecentric als Senior Consultant und Senior Developer tätig. Gerne entwickelt und entwirft er gut testbare Software und bewegt sich dabei sowohl im Backend als auch im Frontend. Holger nutzt dabei oft den Java EE/Spring Stack und ist zudem ein Verfechter von JavaScript- und Web-Technologien.
Er hält immer Ausschau nach innovativen Lösungen für Kundenprobleme. Sein aktuelles Interesse gilt dem Bereich der funktionalen Programmierung, und er bleibt im Bereich JavaScript-Frameworks immer am Ball.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.