Ad-hoc-Einheitentests in NodeJS

In letzter Zeit habe ich rumgejammt Programmieren einer Prototyp-Desktop-App mit Electron und Vue.

Es hat wirklich Spaß gemacht, all diese „Best Practices“ loszulassen und einfach so viel und so schnell wie möglich zu programmieren.

Eine dieser Best Practices, die ich aufgegeben habe, ist das Testen von Einheiten. Ich glaube zu 100 % an den Wert davon, aber nur unter den richtigen Umständen.

In dieser Phase meines Projekts habe ich keine definierte Spezifikation, meine Funktionen kommen und gehen, wie ich möchte, und der Code, den ich schreibe, ist sehr prozedural (z. B. das Verbinden meiner Datenbank mit meinen Vue-Komponenten).

Für mich zeigt sich der wirkliche Vorteil von Unit-Tests, wenn Sie logische Operatoren (dh if this then that) in Ihrem Code verwenden. Davon habe ich jetzt nicht viel.

Aber … es gab eine Komponente, die ein wenig Datenmanipulation erforderte. Ich musste ein Array von Dateipfaden in ein strukturiertes Objekt umwandeln.

Ich muss das drehen:

['./test/specs/a.js', './test/specs/b.js', './test/specs/a/a.js']

In etwa so:

[{
  title: 'test',
  children: [{
    title: 'specs',
    children: [{
      title: 'a.js'
    }, {
      title: 'b.js'
    }, {
      title: 'a',
      children: [{
        title: 'a.js'
      }]
    }]
  }]
}]

Als ich an dem Code arbeitete, wusste ich, dass dies eine großartige Gelegenheit sein würde, Unit-Tests einzusetzen. Ich wusste, was mein Input war, und ich wusste, was ich von meinem Output haben wollte.


Kurzer Hinweis: Ich habe ein Video zusammengestellt, das all dies abdeckt auf meinem YouTube-Kanal


Eine Reihe von Komponententests würde mir wirklich helfen, zu überprüfen, ob mein Code funktioniert, und mir klare Ziele und sofortiges Feedback geben. beides wesentliche Voraussetzungen, um in einen guten Flow-Zustand zu kommen.

Trotzdem wollte ich mich nicht davon ablenken, den eigentlichen Code zu schreiben.

Ich hatte noch keine Unit-Tests für das Projekt geschrieben, also hatte ich noch kein Test-Framework eingerichtet. Sie sind heutzutage nicht zu kompliziert, um sie zum Laufen zu bringen, aber ich wollte wirklich nicht in ein Kaninchenloch steigen, um das beste Framework, die beste Mock-Bibliothek usw App.

Ich brauchte wirklich eine billige, einfache Alternative zu einem Test-Framework, und hier kommt diese Idee des „Ad-hoc“-Komponententests ins Spiel.

Schreiben eines sehr einfachen Unit-Test-Frameworks

Es gibt zwei Hauptfunktionen, die Sie zum Ausführen eines Komponententests benötigen: einen Test-Runner und eine Assertion-Bibliothek.

NodeJS wird mitgeliefert eine einfache Behauptungsbibliothek als Kernmodul. Und ein sehr einfacher Testrunner kann in etwa 10 Codezeilen geschrieben werden.

Damit hatte ich einen grundlegenden Plan, um meinen Code zu testen:

  • Verschieben Sie die zu testende Funktion in eine separate Datei, um das Laden zu vereinfachen
  • Erstellen Sie daneben eine neue Testdatei
  • Laden Sie in dieser Datei die ‘assert’-Bibliothek und meine Funktionsdatei, schreiben Sie einige Tests und fügen Sie meinen Mini-Runner am Ende hinzu.
  • Führen Sie meine Tests in der Befehlszeile mit der node Kl

Verschieben meiner Funktion in eine separate Datei

Technisch gesehen musste ich das nicht tun, aber es gab viele gute Gründe dafür.

Am wichtigsten ist, dass es viel einfacher ist, meine Funktion in meine Testdatei zu laden.

Da ich eine Vue-Anwendung aufbaue, verwende ich die .vue Dateisyntax, die kein reines JavaScript ist.

Das bedeutet, dass ich etwas zaubern muss, damit meine Testdatei versteht, wie diese Vue-Komponente geladen wird, damit ich zu dem Code gelangen kann, den ich testen möchte.

Ich wollte nichts davon tun, also habe ich den Code stattdessen einfach in eine separate Datei verschoben und ihn dann in meiner Vue-Komponente benötigt. Gott sei Dank für die Modulunterstützung in Node/Webpack!

Ein weiterer guter Grund für das Verschieben der Funktionalität, die ich testen wollte, ist, dass ich dadurch gezwungen bin, jede fest codierte Integration in Vue zu entfernen, da dies zu Problemen mit meinen Komponententests führen würde.

Zum Beispiel weise ich am Ende einer meiner Funktionen den endgültigen geparsten Wert meiner Vue-Komponente mit zu this.data = parsedData.

Das war eine dumme Codezeile, die ich schreiben musste, da sie Integrationscode mit funktionalem Code vermischte.

Stattdessen sollte ich das einfach zurückgeben parsedData Wert zurück zu dem Code, der ihn genannt hat, und ihn die Integration übernehmen lassen. Dies würde meinen gesamten funktionalen Code vom Rest trennen und bei der Trennung von Bedenken und dergleichen helfen.

Ohne einen einzigen Test zu schreiben, habe ich meinen Code bereits verbessert, indem ich ein paar schlechte Angewohnheiten aufgeräumt habe (alles in eine einzige Datei werfen und Bedenken in derselben Funktion mischen).

Hier ist eine Dummy-Datei (wir nennen sie doSomething.js), um Ihnen eine Vorstellung davon zu geben, wie meine neue Datei aussieht:

function doSomething(input) {
  
  let output = input * 2

  
  if (output < 10) {
    output = doSomething(output)
  }
  
  
  if (output > 10 && input === 3) {
    
    output += ' was 3'  
  }
  
  
  return output
}

module.exports = {
  doSomething
}

Erstellen meiner Testdatei

Nachdem mein Code ein wenig verschoben und aufgeräumt wurde, kann ich jetzt mit dem Testen beginnen.

Ich habe meine Testdatei im selben Ordner wie meine Funktionsdatei erstellt, da sie dadurch in der Nähe bleiben, damit ich mich daran erinnere, dass die Testdatei dort ist.

Um es zu benennen, nehme ich den Namen, den ich meiner Funktionsdatei gegeben und hinzugefügt habe .test da drin. Also gegeben doSomething.jsnenne ich meine Testdatei doSomething.test.js.

Auf diese Weise kann ich (und jedes Programm, das ich verwende) zwischen Codedateien und Testdateien unterscheiden, obwohl die beiden direkt nebeneinander liegen.

Jetzt ist es an der Zeit, meine Testdatei zu gestalten.

Das erste, was ich tun muss, erfordert meine Funktionsdatei und die Assert-Bibliothek von Node. Das geht ganz einfach:

const assert = require('assert');
const { doSomething } = require('./doSomething.js')

Damit kann ich meinen ersten Test schreiben, der eine einfache Behauptung sein wird doSomething geladen. Ich mache das, indem ich überprüfe, ob es eine Funktion ist:

const actual = typeof doSomething;
assert(actual === "function", `Expected ${actual} to be "function"`);
console.log('Test Passed')

Das ist eigentlich alles, was ich tun muss, um meinen ersten Test geschrieben und einsatzbereit zu haben.

Wenn ich diesen Code über ausführe node doSomething.test.jsund alles ist gut, es sieht so aus:

Terminalausgabe mit Erfolgsmeldung

Wenn etwas mit meinem Code nicht stimmt (sagen wir, ich habe vergessen, diese Funktion zu exportieren), würde die Assertion einen Fehler ausgeben und so aussehen:

Terminalausgabe mit Fehlermeldung

Da die Assertion einen Fehler auslöst, wird die console Die Nachricht wird nie ausgegeben, da der Knoten sofort nach dem Auslösen des Fehlers nicht mehr ausgeführt wird.

Hier ist der bisherige Code

Einfache, effektive Testorganisation

Ich könnte meine Behauptungen weiter so schreiben, aber es würde schnell unhandlich werden, und diese Behauptungsfehlermeldung ist sicher ein hässliches Biest.

Ich würde meine Tests auch wirklich gerne benennen, damit ich eine gute Organisation in Gang bringen und einen Hinweis darauf bekommen kann, worauf der Test prüft, wenn ich es nächste Woche vergesse (zusammen mit der Hilfe bei dieser Fehlermeldung).

Da Fast alles in JavaScript ist ein Objektich sollte meine Tests auch zum Objekt machen!

Ich werde gleich zeigen, warum, aber hier ist, was ich denke:

const tests = {
  'doSomething should be a function' : function () {
    const actual = typeof doSomething;
    assert(actual === "function", `Expected ${actual} to be "function"`);
  }
}

Es ist ein bisschen mehr Code, aber es wird sich in einer Sekunde wirklich auszahlen.

In diesem neuen Format wird meine Prüfung nicht mehr automatisch ausgeführt. Ich muss es am Ende meiner Datei aufrufen, damit die Magie geschieht.

Ich könnte das durch Laufen tun tests['doSomething should be a function']() aber meine Güte, das ist eine aufgeblähte Lösung.

Stattdessen kann ich meine Objekteigenschaften durchlaufen und jede Testfunktion programmgesteuert ausführen.

Ich kann dies tun, indem ich ein Array aus dem hole tests Objekt verwenden Objekt.Schlüsseldann Schleife durch dieses Array mit für jeden.

Object.keys(tests).forEach((test) => {
  tests[test]()
})

Egal was da draußen passiert, teste einfach weiter

Mit dieser Änderung werden jetzt, egal wie viele Tests ich schreibe, sie alle ohne zusätzliche Arbeit am Ende der Datei ausgeführt.

Außer wenn einer von ihnen nicht besteht, wird die Ausführung an diesem Punkt sofort gestoppt.

Das ist irgendwie scheiße.

Lassen Sie uns das beheben, indem Sie verwenden ein try…catch block.

Try...catch Blöcke sind perfekt für Situationen, in denen Sie Code ausführen (normalerweise eine separate Funktion aufrufen) und eine geringe Wahrscheinlichkeit besteht, dass er explodiert.

Anstatt sich mit einem RUD (schnelle außerplanmäßige Demontage), das try...catch block ermöglicht es uns, den Fehler etwas eleganter zu behandeln. Es gibt uns auch die Möglichkeit, den Rest unseres Codes trotz des ausgelösten Fehlers weiter auszuführen.

Um es zu verwenden, verpacken wir die fehleranfällige Funktion in a try Block, dann behandeln Sie alle Fehler in unserem catch Block:

Object.keys(tests).forEach((test) => {
  try {
    tests[test]()
    console.log(`Passed: '${test}'`)
  } catch (e) {
    console.error(`Failed: '${test}' - ${e.message}`)
  }
});

Jetzt werden alle unsere Tests ausgeführt, auch wenn einer von ihnen fehlschlägt. Und wir bringen die Erfolgsmeldung zurück und verschönern die Testfehlermeldung.

Hier ein erfolgreicher Lauf:

Terminalmeldung mit benutzerdefinierter Erfolgsmeldung

Und hier ist ein fehlgeschlagener Lauf:

Terminalmeldung mit benutzerdefinierter Fehlermeldung

Und hier ist der aktualisierte Code

Das ist doch eine viel schönere Fehlermeldung, oder?

Aber es ist gescheitert, sollte das nicht etwas heißen?

Es gibt diese kleinen Dinger namens ‘Ausgangscodes‘, die Programme verwenden, um anderen Programmen mitzuteilen, ob sie erfolgreich ausgeführt wurden oder nicht.

Sie sind wirklich praktisch für Build-Systeme, da Sie den übergeordneten Prozess wissen lassen können, dass der untergeordnete Prozess irgendwie durcheinander geraten ist, was ihm ermöglicht, sich nicht mehr weiterzubewegen, und Ihnen die Möglichkeit gibt, das Problem sofort zu lösen.

Im Knoten, Ausgangscodes werden unter verschiedenen Bedingungen automatisch gesendetaber die beiden wichtigsten sind:

0 – Nichts ist schief gelaufen, Datei wurde wie erhofft ausgeführt
1 – Uncaught Fatal Exception (z. B. etwas ist explodiert)

Als wir unsere Behauptung ohne das explodieren ließen try...catch block, würde NodeJS mit einem Code von 1 beenden und jeden anderen Prozess darüber informieren.

Aber als wir unsere hinzugefügt haben try...catch Block haben wir aufgehört, Fehler zu werfen, und Node fing an, für jeden Testlauf den Code 0 zurückzugeben, auch für diejenigen mit Fehlern.

Diese Exit-Code-Funktionalität war ziemlich nett, und es wäre wirklich cool, sie wieder zu haben.

Nun, das können wir tun; alles, was wir tun müssen, ist Nodes anzurufen process.exit funktionieren und den Status übergeben, den wir senden möchten.

Dazu definieren wir eine Variable, setzen sie auf 0 und ändern sie dann auf 1, wenn einer unserer Tests fehlschlägt. Nachdem alle Tests ausgeführt wurden, senden wir diese Variable an die process.exit Funktion, die Node wissen lässt, was los ist:

let exitCode = 0;
Object.keys(tests).forEach((test) => {
  try {
    tests[test]()
    console.log(`Passed: '${test}'`)
  } catch (e) {
    exitCode = 1
    console.error(`Failed: '${test}' - ${e.message}`)
  }
})

process.exit(exitCode)

Okay, das behebt es für die Computer, aber was ist mit uns Menschen? Wir hätten auch gerne einen Hinweis auf den Status!

Im Moment sehen alle Nachrichten irgendwie gleich aus. Es wäre wirklich schön, wenn die fehlgeschlagenen Tests fett wären und uns wissen lassen würden, dass etwas Ungewöhnliches passiert ist.

Da wir diesen Code im Terminal ausführen, können wir senden Fluchtsequenzen in unsere Konsolenausgabe ein, um die Anzeige zu ändern.

Es gibt zwei, die wir wollen:

  • Hell (“\x1b[1m”)wasimGrundenurfettist[1m”)whichisbasicallyjustbolding
  • Zurücksetzen (“\x1b[0m”)wodurchdieFormatierungzurückgesetztwird;wichtigfürTestsdienacheinemFehlerausgeführtwerden[0m”)whichresetstheformatting;importantfortestsrunafterafailure

Wir können diese Codes genauso wie Strings an unsere ‘Konsolen’-Aufrufe übergeben.

Hier ist, was die aktualisiert console.error Anruf wird sein:

console.error('\x1b[1m', `Failed: '${test}' - ${e.message}`, '\x1b[0m')

Die Einstellung „Hell“ wird am Anfang hinzugefügt, dann wird die Sequenz „Zurücksetzen“ am Ende eingestellt, um die Helligkeit zu verringern.

Nachdem Sie ein paar weitere Tests hinzugefügt haben (absichtlich einen nicht bestanden haben), sieht die Ausgabe so aus:

Vollständige Terminalausgabe mit Erfolgs- und

Und hier ist der aktualisierte Code

Hat das überhaupt Zeit gespart?!?

Das ist also mein Ad-hoc-Testaufbau. Alles in allem habe ich wahrscheinlich mehr Zeit damit verbracht, es auszuprobieren und zu schreiben, als ich damit verbracht hätte, mich nur an eines der gängigen Frameworks zu halten.

Aber ich habe diese Übung wirklich genossen und denke, dass es ein guter Ansatz für einfache Komponententests ist, insbesondere wenn Sie keine externen Abhängigkeiten installieren möchten.

Es ist auch schön, weil ich Tests als kleine Utensilien zum Schreiben von besserem Code behandeln kann, anstatt eine lästige Pflicht, um die Liste der “echten Programmierer” abzuhaken.

Und für diejenigen unter Ihnen, die Code-Coverage-Süchtige sind, haben wir hier ein “100% Coverage”-Abzeichen, das Sie in Ihrer Repo-Readme posten können:

bedeutungslos "100% Abdeckung" Abzeichen :p


Kopfzeile Foto von Artem Sapegin an Unsplash

Similar Posts

Leave a Reply

Your email address will not be published.