Automatisierte Tests werden bei weiteren Aufgaben zum Einsatz kommen und sind auch in realen Projekten weit verbreitet.
Wenn wir eine Funktion schreiben, können wir uns normalerweise vorstellen, was sie tun soll: welche Parameter welche Ergebnisse liefern.
Während der Entwicklung können wir die Funktion überprüfen, indem wir sie ausführen und das Ergebnis mit dem erwarteten vergleichen. Wir können es zum Beispiel in der Konsole tun.
Wenn etwas nicht stimmt, reparieren wir den Code, führen ihn erneut aus, prüfen das Ergebnis – und so weiter, bis es funktioniert.
Aber solche manuellen „Wiederholungen“ sind unvollkommen.
Beim Testen eines Codes durch manuelle Wiederholungen kann es leicht passieren, dass etwas übersehen wird.
Zum Beispiel erstellen wir eine Funktion f
. Habe etwas Code geschrieben und getestet: f(1)
funktioniert, aber f(2)
funktioniert nicht. Wir korrigieren den Code und jetzt funktioniert f(2)
. Sieht komplett aus? Aber wir haben vergessen, f(1)
erneut zu testen. Das kann zu einem Fehler führen.
Das ist sehr typisch. Wenn wir etwas entwickeln, berücksichtigen wir viele mögliche Anwendungsfälle. Es ist jedoch schwer, von einem Programmierer zu erwarten, dass er nach jeder Änderung alle manuell überprüft. So wird es einfacher, eine Sache zu reparieren und eine andere kaputt zu machen.
Automatisiertes Testen bedeutet, dass Tests zusätzlich zum Code separat geschrieben werden. Sie führen unsere Funktionen auf verschiedene Weise aus und vergleichen die Ergebnisse mit den Erwartungen.
Beginnen wir mit einer Technik namens Behavior Driven Development oder kurz BDD.
BDD ist drei Dinge in einem: Tests UND Dokumentation UND Beispiele.
Um BDD zu verstehen, untersuchen wir einen praktischen Fall der Entwicklung.
Nehmen wir an, wir möchten eine Funktion pow(x, n)
erstellen, die x
auf eine ganzzahlige Potenz n
erhöht. Wir gehen davon aus, dass n≥0
ist.
Diese Aufgabe ist nur ein Beispiel: Es gibt den **
Operator in JavaScript, der das kann, aber hier konzentrieren wir uns auf den Entwicklungsablauf, der auch auf komplexere Aufgaben angewendet werden kann.
Bevor wir den Code von pow
erstellen, können wir uns vorstellen, was die Funktion tun soll, und sie beschreiben.
Eine solche Beschreibung wird als Spezifikation oder kurz Spec bezeichnet und enthält Beschreibungen von Anwendungsfällen sowie Tests dafür, wie zum Beispiel:
beschreiben("pow", function() { it("erhöht zur n-ten Potenz", function() { behaupten.equal(pow(2, 3), 8); }); });
Eine Spezifikation besteht aus drei Hauptbausteinen, die Sie oben sehen können:
describe("title", function() { ... })
Welche Funktionalität beschreiben wir? In unserem Fall beschreiben wir die Funktion pow
. Wird verwendet, um „Arbeiter“ zu gruppieren – die it
blockiert.
it("use case description", function() { ... })
Im it
beschreiben wir den jeweiligen Anwendungsfall in einer für Menschen lesbaren Weise , und das zweite Argument ist eine Funktion, die ihn testet.
assert.equal(value1, value2)
Der it
enthaltene Code sollte bei korrekter Implementierung fehlerfrei ausgeführt werden.
Mithilfe der Funktionen assert.*
wird überprüft, ob pow
wie erwartet funktioniert. Hier verwenden wir eines davon – assert.equal
. Es vergleicht Argumente und gibt einen Fehler aus, wenn sie nicht gleich sind. Hier wird überprüft, ob das Ergebnis von pow(2, 3)
gleich 8
ist. Es gibt weitere Arten von Vergleichen und Prüfungen, die wir später hinzufügen werden.
Die Spezifikation kann ausgeführt werden und der it
angegebene Test wird ausgeführt. Das werden wir später sehen.
Der Entwicklungsablauf sieht normalerweise so aus:
Es wird eine erste Spezifikation geschrieben, mit Tests für die grundlegendste Funktionalität.
Eine erste Implementierung wird erstellt.
Um zu überprüfen, ob es funktioniert, führen wir das Test-Framework Mocha aus (weitere Details folgen in Kürze), das die Spezifikation ausführt. Obwohl die Funktionalität nicht vollständig ist, werden Fehler angezeigt. Wir nehmen Korrekturen vor, bis alles funktioniert.
Jetzt haben wir eine funktionierende erste Implementierung mit Tests.
Wir fügen der Spezifikation weitere Anwendungsfälle hinzu, die wahrscheinlich noch nicht von den Implementierungen unterstützt werden. Tests beginnen zu scheitern.
Gehen Sie zu Schritt 3 und aktualisieren Sie die Implementierung, bis die Tests keine Fehler mehr ergeben.
Wiederholen Sie die Schritte 3–6, bis die Funktionalität bereit ist.
Die Entwicklung ist also iterativ . Wir schreiben die Spezifikation, implementieren sie, stellen sicher, dass die Tests erfolgreich sind, schreiben dann weitere Tests, stellen sicher, dass sie funktionieren usw. Am Ende haben wir sowohl eine funktionierende Implementierung als auch Tests dafür.
Sehen wir uns diesen Entwicklungsablauf in unserem praktischen Fall an.
Der erste Schritt ist bereits abgeschlossen: Wir haben eine erste Spezifikation für pow
. Bevor wir nun die Implementierung durchführen, verwenden wir einige JavaScript-Bibliotheken, um die Tests auszuführen, nur um zu sehen, ob sie funktionieren (sie werden alle fehlschlagen).
Hier im Tutorial verwenden wir die folgenden JavaScript-Bibliotheken für Tests:
Mocha – das Kern-Framework: Es stellt allgemeine Testfunktionen bereit, einschließlich describe
und „ it
sowie die Hauptfunktion, die Tests ausführt.
Chai – die Bibliothek mit vielen Behauptungen. Es ermöglicht die Verwendung vieler verschiedener Behauptungen, im Moment brauchen wir nur assert.equal
.
Sinon – eine Bibliothek zum Ausspionieren von Funktionen, Emulieren integrierter Funktionen und mehr, wir werden sie viel später brauchen.
Diese Bibliotheken eignen sich sowohl für browserinterne als auch für serverseitige Tests. Hier betrachten wir die Browservariante.
Die vollständige HTML-Seite mit diesen Frameworks und pow
-Spezifikationen:
<!DOCTYPE html> <html> <Kopf> <!-- Mokka-CSS hinzufügen, um Ergebnisse anzuzeigen --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- Mokka-Framework-Code hinzufügen --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <Skript> mocha.setup('bdd'); // minimales Setup </script> <!-- Chai hinzufügen --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <Skript> // Chai hat eine Menge Zeug, machen wir die Behauptung global let Assert = chai.assert; </script> </head> <Körper> <Skript> Funktion pow(x, n) { /* Funktionscode soll geschrieben werden, jetzt leer */ } </script> <!-- das Skript mit Tests (beschreiben, es...) --> <script src="test.js"></script> <!-- das Element mit id="mocha" enthält Testergebnisse --> <div id="mokka"></div> <!-- Tests durchführen! -> <Skript> mocha.run(); </script> </body> </html>
Die Seite kann in fünf Teile unterteilt werden:
Der <head>
– fügt Bibliotheken und Stile von Drittanbietern für Tests hinzu.
Das <script>
mit der zu testenden Funktion, in unserem Fall – mit dem Code für pow
.
Die Tests – in unserem Fall ein externes Skript test.js
mit describe("pow", ...)
von oben.
Das HTML-Element <div id="mocha">
wird von Mocha zur Ausgabe von Ergebnissen verwendet.
Die Tests werden durch den Befehl mocha.run()
gestartet.
Das Ergebnis:
Derzeit schlägt der Test fehl, es liegt ein Fehler vor. Das ist logisch: Wir haben einen leeren Funktionscode in pow
, also gibt pow(2,3)
undefined
statt 8
zurück.
Beachten wir für die Zukunft, dass es weitere hochrangige Testläufer wie Karma und andere gibt, die es einfach machen, viele verschiedene Tests automatisch auszuführen.
Lassen Sie uns eine einfache Implementierung von pow
erstellen, damit die Tests bestanden werden:
Funktion pow(x, n) { Rückkehr 8; // :) wir betrügen! }
Wow, jetzt funktioniert es!
Was wir getan haben, ist definitiv ein Betrug. Die Funktion funktioniert nicht: Ein Versuch, pow(3,4)
zu berechnen, würde zu einem falschen Ergebnis führen, die Tests werden jedoch bestanden.
…Aber die Situation ist ganz typisch, sie kommt in der Praxis vor. Tests bestehen, aber die Funktion funktioniert falsch. Unsere Spezifikation ist unvollständig. Wir müssen weitere Anwendungsfälle hinzufügen.
Fügen wir einen weiteren Test hinzu, um zu überprüfen, ob pow(3, 4) = 81
.
Wir können hier eine von zwei Möglichkeiten wählen, den Test zu organisieren:
Die erste Variante – fügen Sie eine weitere assert
hinzu it
beschreiben("pow", function() { it("erhöht zur n-ten Potenz", function() { behaupten.equal(pow(2, 3), 8); behaupten.equal(pow(3, 4), 81); }); });
Zweitens: Machen Sie zwei Tests:
beschreiben("pow", function() { it("2 hoch 3 ist 8", function() { behaupten.equal(pow(2, 3), 8); }); it("3 hoch 4 ist 81", function() { behaupten.equal(pow(3, 4), 81); }); });
Der Hauptunterschied besteht darin, dass der it
Block sofort beendet wird, wenn assert
einen Fehler auslöst. Wenn also in der ersten Variante die erste assert
fehlschlägt, sehen wir nie das Ergebnis der zweiten assert
.
Die Trennung der Tests ist nützlich, um mehr Informationen darüber zu erhalten, was vor sich geht. Daher ist die zweite Variante besser.
Und außerdem gibt es noch eine weitere Regel, die es zu beachten gilt.
Ein Test überprüft eine Sache.
Wenn wir uns den Test ansehen und darin zwei unabhängige Prüfungen sehen, ist es besser, ihn in zwei einfachere Prüfungen aufzuteilen.
Machen wir also mit der zweiten Variante weiter.
Das Ergebnis:
Wie zu erwarten war, schlug der zweite Test fehl. Sicher, unsere Funktion gibt immer 8
zurück, während die assert
81
erwartet.
Schreiben wir etwas Realeres, damit die Tests bestehen:
Funktion pow(x, n) { sei Ergebnis = 1; for (sei i = 0; i < n; i++) { Ergebnis *= x; } Ergebnis zurückgeben; }
Um sicherzustellen, dass die Funktion gut funktioniert, testen wir sie auf weitere Werte. Anstatt it
Blöcke manuell zu schreiben, können wir sie wie for
generieren:
beschreiben("pow", function() { Funktion makeTest(x) { let erwartet = x * x * x; it(`${x} in der Potenz 3 ist ${expected}`, function() { behaupten.equal(pow(x, 3), erwartet); }); } for (sei x = 1; x <= 5; x++) { makeTest(x); } });
Das Ergebnis:
Wir werden noch mehr Tests hinzufügen. Beachten wir jedoch vorher, dass die Hilfsfunktionen makeTest
und for
zusammen gruppiert werden sollten. Wir werden makeTest
in anderen Tests nicht benötigen, es wird nur in for
benötigt: Ihre gemeinsame Aufgabe besteht darin, zu überprüfen, wie pow
in die gegebene Potenz ansteigt.
Die Gruppierung erfolgt mit einer verschachtelten describe
:
beschreiben("pow", function() { beschreiben("erhöht x hoch 3", function() { Funktion makeTest(x) { let erwartet = x * x * x; it(`${x} in der Potenz 3 ist ${expected}`, function() { behaupten.equal(pow(x, 3), erwartet); }); } for (sei x = 1; x <= 5; x++) { makeTest(x); } }); // ... weitere Tests folgen hier, beide beschreiben und können hinzugefügt werden });
Die verschachtelte describe
definiert eine neue „Untergruppe“ von Tests. In der Ausgabe können wir die betitelte Einrückung sehen:
In Zukunft können wir weitere it
und auf der obersten Ebene mit eigenen Hilfsfunktionen describe
, sie werden makeTest
nicht sehen.
before/after
und beforeEach/afterEach
Wir können before/after
-Funktionen einrichten, die vor/nach der Ausführung von Tests ausgeführt werden, sowie beforeEach/afterEach
-Funktionen, die vor/nach jedem it
ausgeführt werden.
Zum Beispiel:
beschreiben("test", function() { before(() => warning("Test gestartet – vor allen Tests")); after(() => alarm("Test beendet – nach allen Tests")); beforeEach(() => warning("Vor einem Test – geben Sie einen Test ein")); afterEach(() => warning("Nach einem Test – Test beenden")); it('test 1', () => alarm(1)); it('test 2', () => alarm(2)); });
Die laufende Reihenfolge wird sein:
Tests gestartet – vor allen Tests (vorher) Vor einem Test – Geben Sie einen Test ein (beforeEach) 1 Nach einem Test – einen Test beenden (afterEach) Vor einem Test – Geben Sie einen Test ein (beforeEach) 2 Nach einem Test – einen Test beenden (afterEach) Tests abgeschlossen – nach allen Tests (nachher)
Öffnen Sie das Beispiel in der Sandbox.
Normalerweise werden beforeEach/afterEach
und before/after
verwendet, um eine Initialisierung durchzuführen, Zähler auf Null zu setzen oder etwas anderes zwischen den Tests (oder Testgruppen) zu tun.
Die Grundfunktionalität von pow
ist vollständig. Die erste Iteration der Entwicklung ist abgeschlossen. Wenn wir mit dem Feiern und dem Champagnertrinken fertig sind – lasst uns weitermachen und es verbessern.
Wie gesagt, die Funktion pow(x, n)
soll mit positiven ganzzahligen Werten n
arbeiten.
Um einen mathematischen Fehler anzuzeigen, geben JavaScript-Funktionen normalerweise NaN
zurück. Machen wir dasselbe für ungültige Werte von n
.
Fügen wir zunächst das Verhalten zur Spezifikation hinzu(!):
beschreiben("pow", function() { // ... it("für negatives n ist das Ergebnis NaN", function() { behaupten.isNaN(pow(2, -1)); }); it("für nicht ganzzahliges n ist das Ergebnis NaN", function() { behaupten.isNaN(pow(2, 1.5)); }); });
Das Ergebnis mit neuen Tests:
Die neu hinzugefügten Tests schlagen fehl, da unsere Implementierung sie nicht unterstützt. So wird BDD gemacht: Zuerst schreiben wir fehlgeschlagene Tests und führen dann eine Implementierung dafür durch.
Andere Behauptungen
Bitte beachten Sie die assert.isNaN
: Sie prüft auf NaN
.
Es gibt auch andere Behauptungen im Chai, zum Beispiel:
assert.equal(value1, value2)
– prüft die Gleichheit value1 == value2
.
assert.strictEqual(value1, value2)
– prüft die strikte Gleichheit value1 === value2
.
assert.notEqual
, assert.notStrictEqual
– inverse Prüfungen zu den oben genannten.
assert.isTrue(value)
– prüft, ob der value === true
assert.isFalse(value)
– prüft, ob der value === false
…die vollständige Liste finden Sie in den Dokumenten
Also sollten wir pow
ein paar Zeilen hinzufügen:
Funktion pow(x, n) { wenn (n < 0) NaN zurückgeben; if (Math.round(n) != n) return NaN; sei Ergebnis = 1; for (sei i = 0; i < n; i++) { Ergebnis *= x; } Ergebnis zurückgeben; }
Jetzt funktioniert es, alle Tests bestehen:
Öffnen Sie das vollständige endgültige Beispiel in der Sandbox.
Bei BDD steht zuerst die Spezifikation, gefolgt von der Implementierung. Am Ende haben wir sowohl die Spezifikation als auch den Code.
Die Spezifikation kann auf drei Arten verwendet werden:
Als Tests garantieren sie, dass der Code korrekt funktioniert.
Als Dokumente – die Titel describe
und it
was die Funktion tut.
Als Beispiele – die Tests sind eigentlich Arbeitsbeispiele, die zeigen, wie eine Funktion verwendet werden kann.
Mit der Spezifikation können wir die Funktion sicher verbessern, ändern oder sogar von Grund auf neu schreiben und sicherstellen, dass sie immer noch ordnungsgemäß funktioniert.
Das ist besonders wichtig bei großen Projekten, wenn eine Funktion an vielen Stellen verwendet wird. Wenn wir eine solche Funktion ändern, gibt es einfach keine Möglichkeit, manuell zu überprüfen, ob jeder Ort, der sie verwendet, noch richtig funktioniert.
Ohne Tests haben Menschen zwei Möglichkeiten:
Um die Änderung durchzuführen, egal was passiert. Und dann stoßen unsere Benutzer auf Fehler, da wir wahrscheinlich etwas nicht manuell überprüfen können.
Oder wenn die Strafe für Fehler hart ist, weil es keine Tests gibt, haben die Leute Angst, solche Funktionen zu ändern, und dann ist der Code veraltet, und niemand möchte sich darauf einlassen. Nicht gut für die Entwicklung.
Automatische Tests helfen, diese Probleme zu vermeiden!
Wenn das Projekt mit Tests abgedeckt ist, gibt es dieses Problem einfach nicht. Nach Änderungen können wir innerhalb von Sekunden Tests durchführen und viele durchgeführte Überprüfungen sehen.
Außerdem hat ein gut getesteter Code eine bessere Architektur.
Das liegt natürlich daran, dass automatisch getesteter Code einfacher zu ändern und zu verbessern ist. Aber es gibt noch einen anderen Grund.
Um Tests zu schreiben, sollte der Code so organisiert sein, dass jede Funktion eine klar beschriebene Aufgabe sowie klar definierte Eingaben und Ausgaben hat. Das bedeutet eine gute Architektur von Anfang an.
Im wirklichen Leben ist das manchmal nicht so einfach. Manchmal ist es schwierig, eine Spezifikation vor dem eigentlichen Code zu schreiben, weil noch nicht klar ist, wie er sich verhalten soll. Aber im Allgemeinen macht das Schreiben von Tests die Entwicklung schneller und stabiler.
Später im Tutorial werden Sie auf viele Aufgaben mit integrierten Tests stoßen. So sehen Sie mehr praktische Beispiele.
Das Schreiben von Tests erfordert gute JavaScript-Kenntnisse. Aber wir fangen gerade erst an, es zu lernen. Um alles klarzustellen: Derzeit sind Sie nicht verpflichtet, Tests zu schreiben, aber Sie sollten sie bereits lesen können, auch wenn sie etwas komplexer sind als in diesem Kapitel.
Wichtigkeit: 5
Was ist im folgenden pow
-Test falsch?
it("Erhöht x hoch n", function() { sei x = 5; sei Ergebnis = x; behaupten.equal(pow(x, 1), Ergebnis); Ergebnis *= x; behaupten.equal(pow(x, 2), Ergebnis); Ergebnis *= x; behaupten.equal(pow(x, 3), Ergebnis); });
PS Syntaktisch ist der Test korrekt und besteht.
Der Test zeigt eine der Versuchungen, denen ein Entwickler beim Schreiben von Tests ausgesetzt ist.
Was wir hier haben, sind eigentlich drei Tests, aber als einzelne Funktion mit drei Asserts angelegt.
Manchmal ist es einfacher, auf diese Weise zu schreiben, aber wenn ein Fehler auftritt, ist es viel weniger offensichtlich, was schief gelaufen ist.
Wenn mitten in einem komplexen Ausführungsablauf ein Fehler auftritt, müssen wir die Daten an diesem Punkt ermitteln. Wir müssen den Test tatsächlich debuggen .
Es wäre viel besser, den Test in mehrere it
Blöcke mit klar geschriebenen Ein- und Ausgaben zu unterteilen.
So was:
beschreiben("Erhöht x zur Potenz n", function() { it("5 hoch 1 ist 5", function() { behaupten.equal(pow(5, 1), 5); }); it("5 hoch 2 entspricht 25", function() { behaupten.equal(pow(5, 2), 25); }); it("5 hoch 3 entspricht 125", function() { behaupten.equal(pow(5, 3), 125); }); });
Wir haben das einzelne it
durch describe
und eine Gruppe von it
-Blöcken ersetzt. Wenn nun etwas schiefgeht, können wir deutlich sehen, um welche Daten es sich handelt.
Wir können auch einen einzelnen Test isolieren und im Standalone-Modus ausführen, indem wir it.only
anstelle von it
schreiben:
beschreiben("Erhöht x zur Potenz n", function() { it("5 hoch 1 ist 5", function() { behaupten.equal(pow(5, 1), 5); }); // Mocha führt nur diesen Block aus it.only("5 hoch 2 entspricht 25", function() { behaupten.equal(pow(5, 2), 25); }); it("5 hoch 3 entspricht 125", function() { behaupten.equal(pow(5, 3), 125); }); });