Besseres CoffeeScript-Testen mit Mokka

In letzter Zeit habe ich eine beträchtliche Menge an CoffeeScript-Arbeit geleistet. Ein Problem, auf das ich früh gestoßen war, war das Testen: Ich wollte mein CoffeeScript nicht manuell in JavaScript konvertieren, bevor ich es testen konnte. Stattdessen wollte ich direkt aus CoffeeScript testen. Wie bin ich dazu gekommen? Lesen Sie weiter, um es herauszufinden!

Erneut veröffentlichtes Tutorial

Alle paar Wochen besuchen wir einige der Lieblingsbeiträge unserer Leser aus der gesamten Geschichte der Website. Dieses Tutorial wurde erstmals im November 2012 veröffentlicht.


Node.js und Node Package Manager müssen installiert sein.

Bevor wir fortfahren, möchte ich darauf hinweisen, dass Sie für dieses Tutorial ein anständiges Wissen über CoffeeScript benötigen. Ich werde hier nicht alles erklären. Wenn Sie sich für CoffeeScript interessieren, sollten Sie die hier unter Nettuts + verfügbaren CoffeeScript-Tests oder die CoffeeScript-Dokumentation lesen.

Außerdem müssen Sie Node.js und Node Package Manager (npm) für dieses Lernprogramm installiert haben. Wenn Sie sie nicht installiert haben, müssen Sie sich keine Sorgen machen: Gehen Sie zu nodejs.org und laden Sie das Installationsprogramm für Ihre Plattform herunter. dann installieren Sie es!


Mocha und Chai treffen

Wir werden die Anfänge einer ToDo-Listenanwendung erstellen (Klischee, ich weiß). Dies sind CoffeeScript-Klassen. Anschließend werden wir einige Tests mit Mocha und Chai schreiben, um diese Funktionalität zu testen.

Warum sowohl Mocha als auch Chai? Nun, Mocha ist ein Testframework, aber es enthält nicht die eigentliche Assertionskomponente. Das mag seltsam klingen: Eine Testbibliothek hat nicht viel mehr zu bieten, oder? Nun, in Mochas Fall. Die Funktionen, die mich zur Bibliothek gebracht haben, sind zweifach: die Möglichkeit, Tests über die Befehlszeile auszuführen (anstatt eine HTML-Seite für die Ausführung im Browser zu haben) und die Möglichkeit, Tests in CoffeeScripts auszuführen, ohne konvertieren zu müssen diesen Code zu JavaScript (zumindest manuell: Mocha macht es hinter den Kulissen). Es gibt auch andere Funktionen, über die ich hier nicht sprechen werde, darunter:

  • Sie können asynchronen Code einfach testen.
  • Sie können besonders langsame Tests beobachten.
  • Sie können die Ergebnisse in verschiedenen Formaten ausgeben.

Und weiter und weiter. Mehr dazu auf der Mocha-Homepage. Um Mocha zu installieren, starten Sie einfach npm install -g Mokka, und du bist fertig.

Was Chai betrifft: Es ist eine großartige Assertionsbibliothek, die Schnittstellen für BDD und TDD bietet. Sie können es sowohl im Browser als auch in der Befehlszeile über einen Knoten verwenden. So verwenden wir es heute. Installieren Sie es für Node via npm install -g chai.

Nun, da wir unsere Bibliotheken installiert haben, beginnen wir mit dem Schreiben von Code.


Unser Projekt einrichten

Beginnen wir mit dem Aufbau eines Mini-Projekts. Erstellen Sie einen Projektordner. Dann erstellen Sie zwei weitere Ordner in diesem Ordner: src, und Prüfung. Unser CoffeeScript-Code wird in die src Ordner, und unsere Tests werden eingehen, Sie haben es erraten, die Tests Mappe. Mocha sucht nach einem Prüfung Ordner standardmäßig, so dass wir uns später etwas Tipparbeit sparen.

Mocha sucht nach einem Prüfung Ordner standardmäßig.

Wir werden zwei CoffeeScript-Klassen erstellen: Aufgabe, welches ein todo artikel sein wird, und Aufgabenliste, Dies ist eine Liste von Aufgaben (ja, es ist mehr als ein Array). Wir werden beide in die src / task.coffee Datei. Dann werden die Tests dafür sein test / taskTest.coffee. Natürlich könnten wir sie in ihre eigenen Dateien aufteilen, aber das werden wir heute einfach nicht tun.

Zunächst müssen wir die Chai-Bibliothek importieren und die BDD-Syntax aktivieren. Hier ist wie:

chai = "chai" erfordern chai.should ()

Indem Sie die chai.form Methode, fügen wir tatsächlich eine sollte Eigentum an Objektprotokoll. Dies erlaubt uns, Tests zu schreiben, die folgendermaßen lesen:

task.name.should.equal "einige Zeichenfolge"

Wenn Sie die TDD-Syntax bevorzugen, können Sie Folgendes tun:

Expect = chai.expect

… Damit Sie Tests wie folgt schreiben können:

Expect (task.name) .to.equal "irgendein String"

Wir müssen beide verwenden, wie Sie sehen werden. Wir werden jedoch so oft wie möglich die BDD-Syntax verwenden.

Jetzt müssen wir unsere importieren Aufgabe und Aufgabenliste Klassen:

TaskList, List = erfordert '… / src / task'

Wenn Sie mit dieser Syntax nicht vertraut sind, ist dies die zerstörte Zuweisung von CoffeeScript sowie einige seiner objektbezogenen Zucker. Grundsätzlich unser benötigen call gibt ein Objekt mit zwei Eigenschaften zurück, die unsere Klassen sind. Diese Zeile zieht sie aus diesem Objekt heraus und gibt uns zwei benannte Variablen Aufgabe und Aufgabenliste, von denen jeder auf die jeweilige Klasse verweist.


Erste Tests schreiben

Großartig! Wie wäre es mit einem Test? Die Schönheit der Mocha-Syntax besteht darin, dass ihre Blöcke (beschreiben und es) sind identisch mit Jasmines (beide sind RSpec sehr ähnlich). Hier ist unser erster Test:

Beschreibe 'Task Instanz', -> Task1 = Task2 = Null sollte 'einen Namen haben', -> Task1 = Neu Task 'Katze füttern' Task1.name.should.equal 'Katze füttern'

Wir beginnen mit einem beschreiben call: Alle diese Tests sind für eine Testinstanz bestimmt. Indem man es einstellt test1 = test2 = null außerhalb unserer individuellen Tests können wir diese Werte für mehrere Tests verwenden.

In unserem ersten Test erstellen wir einfach eine Aufgabe und prüfen, ob die name -Eigenschaft den korrekten Wert hat. Bevor wir den Code dafür schreiben, fügen wir zwei weitere Tests hinzu:

"sollte anfangs unvollständig sein", -> task1.status.should.equal "unvollständig" es "sollte abgeschlossen werden können", -> task1.complete (). should.be.true task1.status.should.equal 'Komplett'

Ok, lassen Sie uns diese Tests ausführen, um sicherzustellen, dass sie fehlschlagen. Öffnen Sie dazu eine Eingabeaufforderung und CD in Ihrem Projektordner. Führen Sie dann diesen Befehl aus:

Mokka - Compiler Kaffee: Kaffee-Skript

Mocha sucht standardmäßig nicht nach CoffeeScript, also müssen wir das verwenden --Compiler Flag, um Mocha mitzuteilen, welcher Compiler verwendet werden soll, wenn er eine Datei mit der Kaffee Dateierweiterung. Sie sollten Fehler erhalten, die folgendermaßen aussehen:

Wenn Sie das nicht sehen, erhalten Sie den Fehler Modul '… / src / task' kann nicht gefunden werden, es ist weil dein src / task.coffee Datei existiert noch nicht. Machen Sie die Datei, und Sie sollten die Fehlermeldung erhalten.


Kodierung unserer ersten Funktionen

Nun, da wir Tests nicht bestehen, ist es an der Zeit, den Code zu schreiben, richtig? Mach das auf src / task.coffee Datei und lass uns knacken.

Klasse Task-Konstruktor: (@name) ->

Dies reicht aus, um den ersten Test zu bestehen. Wenn Sie mit dieser Parametersyntax nicht vertraut sind, wird dadurch nur der Wert festgelegt, an den übergeben wurde neue Aufgabe zum @Name (oder dieser Name) Eigentum. Fügen wir dem Konstruktor jedoch eine weitere Zeile hinzu:

@status = 'unvollständig'

Das ist gut. Gehen Sie jetzt zurück zum Terminal und führen Sie unsere Tests erneut durch. Sie werden das finden - warten Sie eine Sekunde, es hat sich nichts geändert! Warum bestehen unsere ersten beiden Tests nicht??

Eigentlich ein einfaches Problem. Da der CoffeeScript-Compiler den Code in jede Datei in einem IIFE (oder in einer anonymen Funktion mit automatischem Aufruf) einschließt, müssen wir alles „exportieren“, auf das wir von anderen Dateien aus zugreifen möchten. Im Browser würden Sie so etwas tun window.Whatever = Was auch immer. Für Node können Sie beide verwenden global oder Exporte. Wir werden verwenden Exporte, seit 1) gilt dies als best practice und 2) darauf haben wir uns beim Einrichten unserer Tests vorbereitet (erinnern Sie sich an unsere benötigen Anruf?). Deshalb am Ende unseres task.coffee Datei, fügen Sie folgendes hinzu:

Wurzel = Exporte? Fenster root.Task = Task

Wenn dies der Fall ist, sollten Sie feststellen, dass zwei unserer drei Tests jetzt bestanden sind:

Damit der letzte Test bestanden wird, müssen wir eine hinzufügen Komplett Methode. Versuche dies:

complete: -> @status = 'complete' ist wahr

Nun bestehen alle Tests:

Es ist jetzt ein guter Zeitpunkt zu erwähnen, dass Mocha eine Reihe verschiedener Berichte enthält: Dies sind nur verschiedene Möglichkeiten, um die Testergebnisse auszugeben. Du kannst rennen Mokka - Reporter um Ihre Optionen zu sehen:

Standardmäßig verwendet Mocha den Punktreporter. Ich bevorzuge jedoch den Spec-Reporter, also gehe ich vor -R spez am Ende des Befehls (-R ist das Flag für die Einstellung des Reporters).


Ein Feature hinzufügen

Lassen Sie uns ein Feature hinzufügen Aufgabe Klasse: Wir lassen Aufgaben von anderen Aufgaben abhängig sein. Wenn die übergeordnete Aufgabe nicht abgeschlossen ist, kann die untergeordnete Aufgabe nicht ausgeführt werden. Wir halten diese Funktion einfach und ermöglichen, dass Aufgaben nur eine Unteraufgabe haben. Wir werden auch nicht auf Rekursivität prüfen. Während es möglich sein wird, zwei Aufgaben als übergeordnetes und untergeordnetes Element festzulegen, werden beide Aufgaben unvollständig.

Testet zuerst!

es 'sollte in der Lage sein, von einer anderen Aufgabe abhängig zu sein', -> Aufgabe1 = neue Aufgabe 'Geschirr spülen' Task2 = neue Aufgabe 'Geschirr trocknen' task2.pendsOn task1 task2.status.should.equal 'abhängig' task2.parent.should .equal task1 task1.child.should.equal.tuff.equal task2 sollte "die Fertigstellung ablehnen, es ist abhängig von einer nicht abgeschlossenen Aufgabe", -> (-> task2.complete ()) . "

Aufgabe Instanzen werden eine haben kommt drauf an Methode, welche Aufgaben die Aufgabe, die ihr übergeordnetes werden wird. Aufgaben, die eine übergeordnete Aufgabe haben, sollten den Status "abhängig" haben. Außerdem erhalten beide Aufgaben entweder eine Elternteil oder Kind Eigenschaft, die auf die entsprechende Taskinstanz verweist.

Im zweiten Test dort sagen wir, dass eine Aufgabe mit einer unvollständigen übergeordneten Aufgabe einen Fehler auslösen soll, wenn es dort ist Komplett Methode wird aufgerufen. Beachten Sie, wie die Testsyntax funktioniert: Wir müssen aufrufen sollte off von einer Funktion und nicht das Ergebnis der Funktion: Deshalb werden die Funktionen in Klammern gesetzt. Auf diese Weise kann die Testbibliothek die Funktion selbst aufrufen und auf Fehler prüfen.

Führen Sie diese Tests aus und Sie werden feststellen, dass beide fehlschlagen. Codierzeit!

dependonsOn: (@parent) -> @ parent.child = @ @status = 'abhängig'

Nochmals sehr einfach: Wir setzen den task-Parameter einfach auf die übergeordnete Task und geben ihr eine untergeordnete Eigenschaft, auf die sie verweist diese Task-Instanz. Dann setzen wir den Status von diese Aufgabe "abhängig" zu sein.

Wenn Sie dies jetzt ausführen, werden Sie feststellen, dass einer unserer Tests erfolgreich ist, der zweite ist jedoch nicht der Fall Komplett Die Methode sucht nicht nach einer unvollständigen übergeordneten Aufgabe. Lass uns das ändern.

abgeschlossen: -> wenn @Patent? und @ parent.status isnt 'done' warf "Abhängige Task '#@parent.name' ist nicht abgeschlossen." @status = 'complete' ist wahr

Hier ist das fertig Komplett Methode: Wenn es eine übergeordnete Aufgabe gibt und diese nicht abgeschlossen ist, wird ein Fehler ausgegeben. Ansonsten erledigen wir die Aufgabe. Nun sollten alle Tests bestehen.


Erstellen der TaskList

Als nächstes bauen wir die Aufgabenliste Klasse. Wieder beginnen wir mit einem Test:

Beschreibe 'TaskList', -> TaskList = null sollte 'ohne Aufgaben beginnen', -> TaskList = Neue TaskList TaskList.tasks.length.should.equal 0 TaskList.length.should.equal 0

Das ist jetzt ein alter Hut für Sie: Wir schaffen eine Aufgabenliste Objekt und Überprüfung seiner Aufgaben und Länge Eigenschaften, um sicherzustellen, dass beide auf Null stehen. Wie du vielleicht erraten könntest, Aufgaben ist ein Array, das die Aufgaben enthält, während Länge ist nur eine praktische Eigenschaft, die wir aktualisieren, wenn Sie Aufgaben hinzufügen oder entfernen. es erspart uns nur das Schreiben list.tasks.length.

Um diesen Test zu bestehen, erstellen wir diesen Konstruktor:

Klasse TaskList-Konstruktor: () -> @tasks = [] @length = 0

Guter Start, und das hat unseren Test bestanden.

Wir möchten in der Lage sein, Aufgaben zu einer Aufgabenliste hinzuzufügen, oder? Wir haben eine hinzufügen Methode, die entweder eine dauern kann Aufgabe Instanz oder eine Zeichenfolge, die in eine konvertiert wird Aufgabe Beispiel.

Unsere Tests:

es 'sollte neue Aufgaben als Aufgaben annehmen', -> Aufgabe = neue Aufgabe 'Milch kaufen' taskList.add Aufgabe taskList.tasks [0] .name.should.equal 'Milch kaufen' taskList.length.should.equal 1 Neue Aufgaben sollten als Zeichenfolge akzeptiert werden ', -> taskList.add' Müll rausnehmen 'taskList.tasks [1] .name.should.equal' Müll rausnehmen 'taskList.length.should.equal 2

Zuerst fügen wir ein Ist hinzu Aufgabe Objekt und überprüfen Sie die taskList.tasks Array, um zu überprüfen, ob es hinzugefügt wurde. Dann fügen wir eine Zeichenfolge hinzu und stellen sicher, dass a Aufgabe Objekt mit dem richtigen Namen wurde dem hinzugefügt Aufgaben Array. In beiden Fällen überprüfen wir die Länge von Aufgabenliste auch, um sicherzustellen, dass es Eigenschaft aktualisiert wird.

Und die Funktion:

add: (task) -> wenn der Typ der Task 'Zeichenfolge' ist @ task.push neu Task task sonst @ task.push task @length = @ task.length

Ziemlich selbsterklärend, denke ich. Und jetzt bestehen unsere Tests:

Natürlich möchten wir vielleicht Aufgaben aus unserer Liste entfernen, oder??

es 'sollte Tasks entfernen', -> i = taskList.length - 1 taskList.remove taskList.tasks [i] Expect (taskList.tasks [i]). to.not.be.ok

Zuerst nennen wir das Löschen Methode (muss noch geschrieben werden), wobei die letzte Aufgabe in der Liste übergeben wird. Sicher, wir könnten den Index nur fest codieren 1, Ich habe es jedoch so gemacht, weil dieser Test flexibel ist: Wenn wir unsere vorherigen Tests geändert oder weitere Tests hinzugefügt haben, muss dies möglicherweise geändert werden. Natürlich müssen wir den letzten entfernen, da andernfalls die Aufgabe nach ihrem Platz ihren Platz einnehmen wird und etwas an diesem Index vorhanden ist, wenn wir erwarten, dass nichts vorhanden ist.

Und wenn wir davon ausgehen, erwarten wir, dass wir das verwenden erwarten von Funktion und Syntax hier statt unserer üblichen sollte. Das ist weil taskList.tasks [i] wird sein nicht definiert, das erbt nicht von Objektprotokoll, und deshalb können wir nicht verwenden sollte.

Oh ja, das müssen wir noch schreiben Löschen Funktion:

remove: (task) -> i = @ task.indexOf task @tasks = @tasks [0… i] .concat @tasks [i + 1…], wenn i> -1 @length = @ task.length

Einige ausgefallene Arrayarbeit, kombiniert mit den CoffeeScript-Serien und Array-Splicing-Abkürzungen, schließen diesen Deal für uns. Wir teilen einfach alle Elemente vor dem zu entfernenden Element und alle darauf befindlichen Elemente auf. das Wir concat diese beiden Arrays zusammen. Natürlich aktualisieren wir @Länge entsprechend. Kannst du sagen, "Bestehen von Tests"?

Lassen Sie uns noch etwas tun. Wir möchten unsere (relativ) gut aussehende Liste der aktuellen Aufgaben ausdrucken. Dies wird unser bisher komplexester (oder zumindest unser längster) Test sein:

 es 'sollte die Liste ausdrucken', -> taskList = neue TaskList task0 = neue Task 'Milch kaufen' task1 = neue Task 'zum Speichern' task2 = neue Task 'eine andere Task' task3 = neue Task 'Subtask' task4 = new Task 'Sub-Sub-Task' taskList.add task0 taskList.add task1 taskList.add task2 taskList.add task3 taskList.add task4 task0.dependsOn task1 task4.dependsOn task3 task3.dependsOn task2 task1.complete () requiredOutput = " "" Aufgaben - Milch kaufen (abhängig von "Zum Einkaufen gehen") - Zum Speichern (abgeschlossen) - Eine andere Aufgabe - Unteraufgabe (Abhängig von "Andere Aufgabe") - Unterunteraufgabe (Abhängig von "Unteraufgabe") ') "" "taskList.print (). sollte

Was ist denn hier los? Zuerst erstellen wir ein neues Aufgabenliste Objekt, so dass wir von vorne anfangen. Dann erstellen wir fünf Aufgaben und fügen sie hinzu Aufgabenliste. Als Nächstes richten wir einige Abhängigkeiten ein. Zum Schluss erledigen wir eine unserer Aufgaben.

Wir verwenden die Heredoc-Syntax von CoffeeScript, um eine mehrzeilige Zeichenfolge zu erstellen. Wie Sie sehen, halten wir es ziemlich einfach. Wenn eine Aufgabe über eine übergeordnete Aufgabe verfügt, wird diese in Klammern nach dem Aufgabennamen angegeben. Wenn eine Aufgabe erledigt ist, setzen wir das auch.

Bereit zum Schreiben der Funktion?

print: -> str = "Aufgaben \ n \ n" für Task in @Tasks str + = "- # task.name" str + = "(abhängig von '# task.parent.name')" if task.parent? str + = '(abgeschlossen)' wenn task.status 'abgeschlossen' ist str + = "\ n" str

Es ist eigentlich ziemlich unkompliziert: Wir schauen nur über die @Aufgaben array und füge sie zu einem String hinzu. Wenn sie ein Elternteil haben, fügen wir das hinzu, und wenn sie vollständig sind, fügen wir auch das hinzu. Beachten Sie, dass wir die Modifikatorform von verwenden ob Aussage, um unseren Code zu verschärfen. Dann geben wir die Zeichenfolge zurück.

Nun sollten alle unsere Tests bestehen:


Einpacken

Fügen Sie einige Funktionen hinzu, um den Überblick zu erhalten.

Das ist das Ausmaß unseres heutigen kleinen Projekts. Sie können den Code oben auf dieser Seite herunterladen. Warum versuchst du nicht, ein paar Funktionen hinzuzufügen, um den Dreh raus zu bekommen? Hier sind ein paar Ideen:

  • Verhindern Aufgabe Instanzen, um sich aufeinander verlassen zu können (rekursive Abhängigkeiten).
  • Mach das TaskList :: add Eine Methode löst einen Fehler aus, wenn sie etwas anderes als eine Zeichenfolge oder eine Aufgabe Objekt.

Heutzutage finde ich CoffeeScript immer attraktiver, aber der größte Nachteil ist, dass es zu JavaScript kompiliert werden muss, bevor es nützlich ist. Ich bin dankbar für alles, was einen Teil des Workflow-Breakers negiert, und Mocha macht das definitiv. Natürlich ist es nicht perfekt (da es vor dem Ausführen des Codes zu JS kompiliert wurde, stimmen die fehlerhaften Zeilennummern nicht mit Ihren CoffeeScript-Zeilennummern überein), aber es ist für mich ein Schritt in die richtige Richtung!

Wie ist es mit Ihnen? Wenn Sie CoffeeScript verwenden, wie haben Sie getestet? Lass es mich in den Kommentaren wissen.