Schon das kleinste JavaScript-Projekt kann von Unit Testing profitieren. Aber gerade in kleinen Projekten können Lösungen wie Mocha oder Jest sich als viel zu schwergewichtig anfühlen.

Aber warum eigentlich ein Framework verwenden? Wozu das Projekt mit Dependencies vollstopfen, wenn ein paar elegante Zeilen Code für unsere Zwecke reichen?

In seiner einfachsten Form ist Unit Testing nichts anderes als das Vergleichen von Annahmen mit der Ausgabe von tatsächlichen Code. In JavaScript gibt es dafür eine eingebaute Lösung: console.assert().

const starDestroyer = new StarDestroyer();
console.assert(starDestroyer !== null, 'Star destroyer exists');
console.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');
console.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');

Im Browser wie auch in Node.js erzeugt diese Methode eine Konsolen-Ausgabe, wenn die Annahme nicht zutrifft.

Scoping

Wenn in ein und derselben Datei mehrere Testfälle geprüft werden sollen, ist eine klares Scoping von Variablen notwendig. So wird sichergestellt, das Ergebnisse aus einem Test nicht irrtümlicherweise im nächsten Test verwendet werden. In JavaScript ist die Erzeugung eines neuen Scopes jederzeit mittels {…} möglich:

{
  const starDestroyer = new StarDestroyer();
  console.assert(starDestroyer !== null, 'Star destroyer exists');
  console.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');
  console.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');
}

// No starDestroyer here

Mehrere Tests können so in einer Datei abgehandelt werden.

Für automatisiertes Testing ist die Verwendung von console.assert natürlich zu kurz gesprungen, denn hier kann nur durch Sichtkontrolle überprüft werden, ob alle Tests sauber durchgelaufen sind. Wichtig wäre es also, auf der Kommandozeile einen Exit-Code zu bekommen, der auf einen Fehler hinweist.

Das eingebaute Assert in Node.js

In Node.js existiert dafür Assert:

const assert = require('node:assert');

{
  const starDestroyer = new StarDestroyer();
  assert.ok(starDestroyer !== null, 'Star destroyer exists');
  assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');
  assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');
}

Im Fehlerfall wird assert.AssertionError geworfen, der auf der CLI einen auch für Automatisierungen wahrnehmbaren Exit-Code erzeugt.

Der Nachteil dieser Lösung ist gut erkennbar: Diese Art von Testing funktioniert nur in Node.js, aber nicht im Browser.

Lösung Marke Eigenbau

Gott sei Dank ist die Idee mit dem Werfen von Errors aber gar nicht so komplex. Und damit können wir uns eine kleine Funktion konstruieren, mit der wir ganz grundsätzliches Testing durchführen können:

const assertOk = (assertion, message = 'Assertion') => {
  console.log((assertion ? "✅" : "💥") + " " + message);
  if (!assertion) {
    throw new Error(`"${message}" failed`);
  }
};

Damit schlagen wir mehrere Fliegen mit einer Klappe:

  • Jeder Testfall erzeugt eine Ausgabe, ob erfolgreich oder unerfolgreich.
  • Die Tests können sowohl im Browser als auch mit Node.js ausgeführt werden.
  • Ein unerfolgreicher Testfall erzeugt einen Fehler, der auch auf der CLI für einen Exit-Code sorgt.

Etwas aussagekräftiger könnte die Funktion sogar so aussehen:

const assertStrictEqual = (a, b = true, message = '') => {
    message === '' || (message += " | ");
    message += message = `'${a}' equals '${b}'`;

    console.log((a === b ? "✅" : "💥") + " " + message);
    if (a !== b) {
      throw new Error(`"${message}" failed`);
    }
};

Damit können nicht nur direkt Vergleiche durchgeführt werden, sondern auch gleich eine aussagekräftige Nachricht über den tatsächlichen Inhalt des Tests mit ausgegeben werden.

Wenn ihr ein Drop-In-Replacement für assert von Node.js haben wollt, könnt ihr die beiden Funktionen in statische Methoden verwandeln:

const assert = {
  /**
   * Check if `assertion` is `true`. Throw Error on error.
   * @param {Boolean} assertion
   * @param {String} message
   */
  ok(assertion, message = 'Assertion') {
    console.log((assertion ? "✅" : "💥") + " " + message);
    if (!assertion) {
      throw new Error(`"${message}" failed`);
    }
  },

  /**
   * Check if `actual` strictly equals `b`. Throw Error on error.
   * @param {any} actual
   * @param {any} expected
   * @param {String} message
   */
  strictEqual(actual, expected, message = '') {
    message === '' || (message += " | ");
    message += message = `'${actual}' equals '${expected}'`;

    assert.ok(actual == expected, message)
  }
};

Oh, und wenn ihr eure Tests noch gruppieren wollt:

console.group('Testing StarDestroyer');
{
  const starDestroyer = new StarDestroyer();
  assert.ok(starDestroyer !== null, 'Star destroyer exists');
  assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');
  assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');
}
console.groupEnd();

…erzeugt eine schön gruppierte Ausgabe eurer Tests.

Voilà! Eine Beispiel-Ausgabe:

GeoJson.Feature
  ✅ Type matches | 'Feature' equals 'Feature'
  ✅ Feature.properties.title | 'Test' equals 'Test'
  ✅ Feature.geometry.type | 'Point' equals 'Point'
  ✅ No id
GeoJson.FeatureCollection
  ✅ 'FeatureCollection' equals 'FeatureCollection'
  ✅ Bounding Box West | '6.5664576' equals '6.5664576'
  ✅ Bounding Box South | '51.04571' equals '51.04571'
  ✅ Bounding Box East | '1.53946' equals '1.53946'
  ✅ Bounding Box North | '58.109285' equals '58.109285'
  ✅ FeatureCollection | 'FeatureCollection' equals 'FeatureCollection'
  ✅ Two Features exist | '2' equals '2'
  ✅ Feature.type | 'Feature' equals 'Feature'
  ✅ Feature.properties.title | 'Sailing boat' equals 'Sailing boat'

Fazit

Natürlich ersetzt diese sehr simple Herangehensweise nicht wirklich echte Unit-Test-Frameworks. So ist zum Beispiel der Vergleich von Objekt-Strukturen nicht möglich, das Testen von Exceptions oder auch die Verwendung von Mocking und ähnlichen Features. Aber zumindest senkt es die Hürde, überhaupt mit Testing zu beginnen. 😉


Und hier gibt es weitere Artikel zum Thema · · · .

Zuletzt geändert am

fboës - Der Blog