Soll savePeople () Unit-getestet werden?
Ja, das sollte es. Versuchen Sie jedoch, Ihre Testbedingungen unabhängig von der Implementierung zu schreiben. Verwandeln Sie beispielsweise Ihr Verwendungsbeispiel in einen Komponententest:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Dieser Test macht mehrere Dinge:
- es überprüft den Vertrag der Funktion
savePeople()
- Es kümmert sich nicht um die Implementierung von
savePeople()
- es dokumentiert die beispielhafte Verwendung von
savePeople()
Beachten Sie, dass Sie den Datenspeicher weiterhin verspotten / stubben / fälschen können. In diesem Fall würde ich nicht nach expliziten Funktionsaufrufen suchen, sondern nach dem Ergebnis der Operation. Auf diese Weise ist mein Test auf zukünftige Änderungen / Refaktoren vorbereitet.
Beispielsweise könnte Ihre Datenspeicherimplementierung saveBulkPerson()
in Zukunft eine Methode bereitstellen - jetzt würde eine Änderung der zu verwendenden Implementierung savePeople()
den saveBulkPerson()
Komponententest nicht abbrechen, solange dies saveBulkPerson()
wie erwartet funktioniert. Und wenn saveBulkPerson()
irgendwie nicht funktioniert wie erwartet, Ihr Gerät zu testen wird , dass fangen.
oder würde ein solcher Test das eingebaute forEach-Sprachkonstrukt testen?
Versuchen Sie, wie gesagt, die erwarteten Ergebnisse und die Funktionsschnittstelle zu testen, nicht die Implementierung (es sei denn, Sie führen Integrationstests durch - dann kann das Abfangen bestimmter Funktionsaufrufe hilfreich sein). Wenn es mehrere Möglichkeiten gibt, eine Funktion zu implementieren, sollten alle mit Ihrem Komponententest funktionieren.
In Bezug auf Ihre Aktualisierung der Frage:
Test auf Zustandsänderungen! ZB wird ein Teil des Teigs verwendet. Stellen Sie gemäß Ihrer Implementierung sicher, dass die verwendete Menge dough
in die Menge passt, pan
oder dass die Menge dough
aufgebraucht ist. Stellen Sie pan
nach dem Funktionsaufruf sicher, dass das Cookies enthält. Stellen Sie sicher, dass das oven
leer ist / sich im selben Zustand wie zuvor befindet.
Überprüfen Sie für zusätzliche Tests die Flankenfälle: Was passiert, wenn die oven
vor dem Anruf nicht leer ist? Was passiert, wenn nicht genug da ist dough
? Ist der pan
schon voll?
Sie sollten in der Lage sein, alle erforderlichen Daten für diese Tests aus den Objekten Teig, Pfanne und Ofen selbst abzuleiten. Die Funktionsaufrufe müssen nicht erfasst werden. Behandeln Sie die Funktion so, als stünde Ihnen ihre Implementierung nicht zur Verfügung!
Tatsächlich schreiben die meisten TDD-Benutzer ihre Tests, bevor sie die Funktion schreiben, sodass sie nicht von der tatsächlichen Implementierung abhängig sind.
Für Ihre neueste Ergänzung:
Wenn ein Benutzer ein neues Konto erstellt, müssen einige Dinge geschehen: 1) Ein neuer Benutzerdatensatz muss in der Datenbank erstellt werden. 2) Eine Begrüßungs-E-Mail muss gesendet werden. 3) Die IP-Adresse des Benutzers muss für Betrugsfälle aufgezeichnet werden Zwecke.
Deshalb möchten wir eine Methode erstellen, die alle Schritte "Neuer Benutzer" miteinander verbindet:
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Für eine Funktion wie diese würde ich die Parameter dataStore
und verspotten / stub / fake (was auch immer allgemeiner erscheint) emailService
. Diese Funktion führt keine Zustandsübergänge für einen Parameter aus, sondern delegiert sie an Methoden einiger von ihnen. Ich würde versuchen, zu überprüfen, dass der Aufruf der Funktion 4 Dinge tat:
- Es wurde ein Benutzer in den Datenspeicher eingefügt
- Es hat eine Begrüßungs-E-Mail gesendet (oder zumindest die entsprechende Methode aufgerufen)
- Es zeichnete die IP des Benutzers im Datenspeicher auf
- Es hat alle aufgetretenen Ausnahmen / Fehler delegiert (falls vorhanden).
Die ersten drei Prüfungen können mit Schein, Stich oder Fälschung von dataStore
und durchgeführt werden emailService
(Sie möchten beim Testen wirklich keine E-Mails senden). Da ich dies für einige der Kommentare nachschlagen musste, sind dies die Unterschiede:
- Eine Fälschung ist ein Objekt, das sich wie das Original verhält und bis zu einem gewissen Grad nicht zu unterscheiden ist. Sein Code kann normalerweise über Tests hinweg wiederverwendet werden. Dies kann beispielsweise eine einfache speicherinterne Datenbank für einen Datenbank-Wrapper sein.
- Ein Stub implementiert nur so viel wie nötig, um die für diesen Test erforderlichen Operationen auszuführen. In den meisten Fällen ist ein Stub spezifisch für einen Test oder eine Testgruppe, für die nur ein kleiner Satz der Methoden des Originals erforderlich ist. In diesem Beispiel könnte es sich um eine handeln
dataStore
, die nur eine geeignete Version von insertUserRecord()
und implementiert recordIpAddress()
.
- Ein Mock ist ein Objekt, mit dem Sie überprüfen können, wie es verwendet wird (in den meisten Fällen können Sie Aufrufe seiner Methoden auswerten). Ich würde versuchen, sie in Komponententests sparsam zu verwenden, da Sie mit ihnen tatsächlich versuchen, die Funktionsimplementierung und nicht die Einhaltung der Schnittstelle zu testen, aber sie haben immer noch ihre Verwendung. Es gibt viele Modell-Frameworks, mit denen Sie genau das Modell erstellen können, das Sie benötigen.
Beachten Sie, dass, wenn eine dieser Methoden einen Fehler auslöst, der Fehler in den aufrufenden Code aufsteigen soll, damit er den Fehler nach Belieben behandeln kann. Wenn er vom API-Code aufgerufen wird, übersetzt er den Fehler möglicherweise in einen geeigneten HTTP-Antwortcode. Wenn es von einer Webschnittstelle aufgerufen wird, übersetzt es den Fehler möglicherweise in eine entsprechende Meldung, die dem Benutzer angezeigt wird, und so weiter. Der Punkt ist, dass diese Funktion nicht weiß, wie sie mit den Fehlern umgeht, die möglicherweise ausgelöst werden.
Erwartete Ausnahmen / Fehler sind gültige Testfälle: Sie bestätigen, dass sich die Funktion in einem solchen Fall wie erwartet verhält. Dies kann erreicht werden, indem das entsprechende Schein- / Fälschungs- / Stichobjekt geworfen wird, wenn dies gewünscht wird.
Das Wesen meiner Verwirrung ist, dass es zum Testen einer solchen Funktion in einer Einheit notwendig erscheint, die genaue Implementierung im Test selbst zu wiederholen (indem angegeben wird, dass Methoden in einer bestimmten Reihenfolge auf Mocks angewendet werden), und dass dies falsch erscheint.
Manchmal muss das gemacht werden (obwohl es Ihnen bei Integrationstests am wichtigsten ist). Häufiger gibt es andere Möglichkeiten, um die erwarteten Nebenwirkungen / Zustandsänderungen zu überprüfen.
Das Überprüfen der genauen Funktionsaufrufe führt zu eher spröden Komponententests: Nur kleine Änderungen an der ursprünglichen Funktion führen zum Fehlschlagen. Dies kann erwünscht sein oder nicht, erfordert jedoch eine Änderung der entsprechenden Komponententests, wenn Sie eine Funktion ändern (Refactoring, Optimierung, Fehlerbehebung, ...).
Leider verliert der Komponententest in diesem Fall etwas an Glaubwürdigkeit: Da er geändert wurde, bestätigt er die Funktion nicht, nachdem sich die Änderung wie zuvor verhalten hat.
Angenommen, jemand fügt oven.preheat()
in Ihrem Beispiel für das Backen von Cookies einen Aufruf für (Optimierung!) Hinzu:
- Wenn Sie das Ofenobjekt verspottet haben, wird es diesen Aufruf nicht erwarten und den Test nicht bestehen, obwohl sich das beobachtbare Verhalten der Methode nicht geändert hat (Sie haben hoffentlich immer noch eine Pfanne mit Cookies).
- Ein Stub kann fehlschlagen oder nicht, je nachdem, ob Sie nur die zu testenden Methoden oder die gesamte Schnittstelle mit einigen Dummy-Methoden hinzugefügt haben.
- Eine Fälschung sollte nicht scheitern, da sie die Methode (entsprechend der Schnittstelle) implementieren sollte
Bei meinen Unit-Tests versuche ich, so allgemein wie möglich zu sein: Wenn sich die Implementierung ändert, aber das sichtbare Verhalten (aus Sicht des Aufrufers) immer noch dasselbe ist, sollten meine Tests bestanden werden. Im Idealfall sollte der einzige Fall, in dem ich einen vorhandenen Komponententest ändern muss, eine Fehlerbehebung sein (des Tests, nicht der getesteten Funktion).