it-swarm.com.de

Wo liegt die Grenze zwischen Unit-Testing-Anwendungslogik und misstrauischen Sprachkonstrukten?

Betrachten Sie eine Funktion wie diese:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Es könnte so verwendet werden:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Nehmen wir an , dass Store eigene Komponententests hat oder vom Hersteller bereitgestellt wird. In jedem Fall vertrauen wir Store. Nehmen wir weiter an, dass die Fehlerbehandlung - z. B. bei Fehlern beim Trennen der Datenbank - nicht in der Verantwortung von savePeople liegt. Nehmen wir in der Tat an, dass der Speicher selbst eine magische Datenbank ist, die unmöglich in irgendeiner Weise fehlerhaft sein kann. Unter diesen Voraussetzungen lautet die Frage:

Sollte savePeople() Unit-getestet werden, oder würden solche Tests dem Testen des integrierten forEach Sprachkonstrukts gleichkommen?

Wir könnten natürlich einen Schein dataStore übergeben und behaupten, dass dataStore.savePerson() für jede Person einmal aufgerufen wird. Sie könnten sicherlich das Argument vorbringen, dass ein solcher Test Sicherheit gegen Implementierungsänderungen bietet: z. B. wenn wir uns entschieden haben, forEach durch eine herkömmliche for -Schleife oder eine andere Iterationsmethode zu ersetzen. Der Test ist also nicht ganz trivial. Und doch scheint es schrecklich nah ...


Hier ist ein weiteres Beispiel, das möglicherweise fruchtbarer ist. Stellen Sie sich eine Funktion vor, die nur andere Objekte oder Funktionen koordiniert. Zum Beispiel:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

Wie sollte eine Funktion wie diese auf Einheit getestet werden, vorausgesetzt, Sie denken, dass dies der Fall sein sollte? Es fällt mir schwer, mir einen Unit-Test vorzustellen, der nicht einfach dough, pan und oven verspottet und dann behauptet, dass Methoden für sie aufgerufen werden. Bei einem solchen Test wird jedoch lediglich die genaue Implementierung der Funktion dupliziert.

Zeigt diese Unfähigkeit, die Funktion auf sinnvolle Weise zu testen, einen Konstruktionsfehler mit der Funktion selbst an? Wenn ja, wie könnte es verbessert werden?


Um der Frage, die das Beispiel bakeCookies motiviert, noch mehr Klarheit zu verleihen, füge ich ein realistischeres Szenario hinzu, auf das ich beim Versuch gestoßen bin, Tests zu Legacy-Code hinzuzufügen und diesen umzugestalten.

Wenn ein Benutzer ein neues Konto erstellt, müssen einige Dinge geschehen: 1) Es muss ein neuer Benutzerdatensatz in der Datenbank erstellt werden. 2) Eine Willkommens-E-Mail muss gesendet werden. 3) Die IP-Adresse des Benutzers muss wegen Betrugs aufgezeichnet werden Zwecke.

Wir möchten also eine Methode erstellen, die alle Schritte des "neuen Benutzers" miteinander verbindet:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Beachten Sie, dass, wenn eine dieser Methoden einen Fehler auslöst, der Fehler bis zum aufrufenden Code hochsprudeln soll, damit er den Fehler nach eigenem Ermessen behandeln kann. Wenn es vom API-Code aufgerufen wird, kann es den Fehler in einen geeigneten http-Antwortcode übersetzen. Wenn es von einer Weboberfläche aufgerufen wird, übersetzt es den Fehler möglicherweise in eine entsprechende Nachricht, die dem Benutzer angezeigt werden soll, und so weiter. Der Punkt ist , dass diese Funktion nicht weiß, wie sie mit den Fehlern umgehen soll, die möglicherweise ausgelöst werden.

Das Wesentliche meiner Verwirrung ist, dass es zum Unit-Test einer solchen Funktion notwendig erscheint, die genaue Implementierung im Test selbst zu wiederholen (indem angegeben wird, dass Methoden in einer bestimmten Reihenfolge für Mocks aufgerufen werden), und das scheint falsch zu sein.

88
Jonah

Sollte savePeople() Unit-getestet werden? Ja. Sie testen das nicht dataStore.savePerson funktioniert oder dass die Datenbankverbindung funktioniert oder sogar, dass foreach funktioniert. Sie testen, ob savePeople das Versprechen erfüllt, das es durch seinen Vertrag macht.

Stellen Sie sich dieses Szenario vor: Jemand führt einen großen Refactor der Codebasis durch und entfernt versehentlich den Teil forEach der Implementierung, sodass immer nur das erste Element gespeichert wird. Möchten Sie nicht, dass ein Unit-Test das erfasst?

117
Bryan Oakley

Normalerweise taucht diese Art von Frage auf, wenn Leute eine "Test-After" -Entwicklung durchführen. Gehen Sie dieses Problem aus der Sicht von TDD an, wo Tests vor der Implementierung stattfinden, und stellen Sie sich diese Frage erneut als Übung.

Zumindest in meiner Anwendung von TDD, die normalerweise von außen nach innen erfolgt, würde ich nach der Implementierung von savePeople keine Funktion wie savePerson implementieren. Die Funktionen savePeople und savePerson würden als eins beginnen und von denselben Komponententests testgetrieben werden. Die Trennung zwischen den beiden würde sich nach einigen Tests im Refactoring-Schritt ergeben. Diese Arbeitsweise würde auch die Frage aufwerfen, wo sich die Funktion savePeople befinden sollte - ob es sich um eine freie Funktion oder um einen Teil von dataStore handelt.

Am Ende würden die Tests nicht nur prüfen, ob Sie ein Person im Store korrekt speichern können, sondern auch viele Personen. Dies würde mich auch zu der Frage führen, ob andere Überprüfungen erforderlich sind, z. B. "Muss ich sicherstellen, dass die Funktion savePeople atomar ist und entweder alle oder keine speichert?", "Kann sie nur irgendwie Fehler zurückgeben für die Menschen, die nicht gerettet werden konnten? Wie würden diese Fehler aussehen? "und so weiter. All dies ist viel mehr als nur die Überprüfung der Verwendung eines forEach oder anderer Iterationsformen.

Wenn die Anforderung, mehr als eine Person gleichzeitig zu speichern, erst kam, nachdem savePerson bereits geliefert wurde, würde ich die vorhandenen Tests von savePerson aktualisieren, um die neue Funktion savePeople, um sicherzustellen, dass immer noch eine Person gerettet werden kann, indem Sie zunächst einfach delegieren und dann das Verhalten für mehr als eine Person durch neue Tests testen und überlegen, ob es notwendig wäre, das Verhalten atomar zu machen oder nicht.

36
MichelHenrich

Sollte savePeople () Unit-getestet werden

Ja, das sollte es. Versuchen Sie jedoch, Ihre Testbedingungen unabhängig von der Implementierung zu schreiben. Verwandeln Sie Ihr Anwendungsbeispiel beispielsweise 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()
  • die Implementierung von savePeople() ist ihm egal
  • es dokumentiert die beispielhafte Verwendung von savePeople()

Beachten Sie, dass Sie den Datenspeicher immer noch verspotten/stub/fälschen können. In diesem Fall würde ich nicht nach expliziten Funktionsaufrufen suchen, sondern nach dem Ergebnis der Operation. Auf diese Weise wird mein Test auf zukünftige Änderungen/Refaktoren vorbereitet.

Beispielsweise könnte Ihre Datenspeicherimplementierung in Zukunft eine saveBulkPerson() -Methode bereitstellen - jetzt würde eine Änderung der Implementierung von savePeople() zur Verwendung von saveBulkPerson() die Einheit nicht beschädigen Test solange saveBulkPerson() wie erwartet funktioniert. Und wenn saveBulkPerson() irgendwie nicht wie erwartet funktioniert, fängt Ihr Unit-Test will das ab.

oder würden solche Tests dem Testen des integrierten forEach-Sprachkonstrukts gleichkommen?

Versuchen Sie wie gesagt, auf erwartete Ergebnisse und die Funktionsschnittstelle zu testen, nicht auf die Implementierung (es sei denn, Sie führen Integrationstests durch - dann kann es hilfreich sein, bestimmte Funktionsaufrufe abzufangen). Wenn es mehrere Möglichkeiten gibt, eine Funktion zu implementieren, sollten alle mit Ihrem Komponententest funktionieren.

In Bezug auf Ihr Update der Frage:

Auf Zustandsänderungen prüfen! Z.B. Ein Teil des Teigs wird verwendet. Stellen Sie gemäß Ihrer Implementierung sicher, dass die Menge des verwendeten dough in pan passt, oder behaupten Sie, dass das dough aufgebraucht ist. Stellen Sie sicher, dass pan nach dem Funktionsaufruf Cookies enthält. Stellen Sie sicher, dass oven leer ist/sich im selben Zustand wie zuvor befindet.

Überprüfen Sie für zusätzliche Tests die Edge-Fälle: Was passiert, wenn oven vor dem Aufruf nicht leer ist? Was passiert, wenn nicht genug dough vorhanden ist? Ist das pan schon voll?

Sie sollten in der Lage sein, alle für diese Tests erforderlichen Daten aus den Teig-, Pfannen- und Ofenobjekten selbst abzuleiten. Die Funktionsaufrufe müssen nicht erfasst werden. Behandeln Sie die Funktion so, als ob Ihnen ihre Implementierung nicht zur Verfügung stehen würde!

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) Es muss ein neuer Benutzerdatensatz in der Datenbank erstellt werden. 2) Eine Willkommens-E-Mail muss gesendet werden. 3) Die IP-Adresse des Benutzers muss wegen Betrugs aufgezeichnet werden Zwecke.

Wir möchten also eine Methode erstellen, die alle Schritte des "neuen Benutzers" 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 emailService verspotten/stub/fake (was auch immer allgemeiner erscheint). Diese Funktion führt keine Zustandsübergänge für einen Parameter selbst durch, sondern delegiert sie an Methoden einiger von ihnen. Ich würde versuchen zu überprüfen, ob der Aufruf der Funktion 4 Dinge getan hat:

  • es wurde ein Benutzer in den Datenspeicher eingefügt
  • es wurde eine Begrüßungs-E-Mail gesendet (oder zumindest die entsprechende Methode genannt)
  • es zeichnete die IP des Benutzers im Datenspeicher auf
  • es hat alle aufgetretenen Ausnahmen/Fehler delegiert (falls vorhanden).

Die ersten 3 Überprüfungen können mit Mocks, Stubs oder Fakes von dataStore und emailService durchgeführt werden (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. Der Code kann normalerweise testübergreifend wiederverwendet werden. Dies kann beispielsweise eine einfache In-Memory-Datenbank für einen Datenbank-Wrapper sein.
  • Ein Stub implementiert nur so viel wie nötig, um die erforderlichen Operationen dieses Tests zu erfüllen. In den meisten Fällen ist ein Stub spezifisch für einen Test oder eine Gruppe von Tests, für die nur ein kleiner Satz der Methoden des Originals erforderlich ist. In diesem Beispiel könnte es sich um ein dataStore handeln, das nur eine geeignete Version von insertUserRecord() und recordIpAddress() implementiert.
  • Ein Mock ist ein Objekt, mit dem Sie überprüfen können, wie es verwendet wird (meistens, indem 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 Mock-Frameworks, mit denen Sie genau das Mock erstellen können, das Sie benötigen.

Beachten Sie, dass, wenn eine dieser Methoden einen Fehler auslöst, der Fehler bis zum aufrufenden Code hochsprudeln soll, damit er den Fehler nach eigenem Ermessen behandeln kann. Wenn es vom API-Code aufgerufen wird, kann es den Fehler in einen geeigneten HTTP-Antwortcode übersetzen. Wenn es von einer Weboberfläche aufgerufen wird, übersetzt es den Fehler möglicherweise in eine entsprechende Nachricht, die dem Benutzer angezeigt werden soll, und so weiter. Der Punkt ist, dass diese Funktion nicht weiß, wie sie mit den Fehlern umgehen soll, die möglicherweise ausgelöst werden.

Erwartete Ausnahmen/Fehler sind gültige Testfälle: Sie bestätigen, dass sich die Funktion im Falle eines solchen Ereignisses so verhält, wie Sie es erwarten. Dies kann erreicht werden, indem das entsprechende Mock/Fake/Stub-Objekt auf Wunsch geworfen wird.

Das Wesentliche meiner Verwirrung ist, dass es zum Unit-Test einer solchen Funktion notwendig erscheint, die genaue Implementierung im Test selbst zu wiederholen (indem angegeben wird, dass Methoden in einer bestimmten Reihenfolge für Mocks aufgerufen werden), und das scheint falsch zu sein.

Manchmal muss dies getan werden (obwohl Sie sich bei Integrationstests hauptsächlich darum kümmern). 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 ziemlich spröden Komponententests: Nur kleine Änderungen an der ursprünglichen Funktion führen dazu, dass sie fehlschlagen. Dies kann erwünscht sein oder nicht, erfordert jedoch eine Änderung der entsprechenden Komponententests, wenn Sie eine Funktion ändern (sei es Refactoring, Optimierung, Fehlerbehebung, ...).

In diesem Fall verliert der Komponententest leider etwas an Glaubwürdigkeit: Da er geändert wurde, bestätigt er die Funktion nicht, nachdem sich die Änderung wie zuvor verhalten hat.

Stellen Sie sich zum Beispiel vor, jemand fügt oven.preheat() (Optimierung!) In Ihrem Cookie-Backbeispiel einen Aufruf hinzu:

  • Wenn Sie das Ofenobjekt verspottet haben, wird dieser Aufruf nicht erwartet und der Test wird nicht bestanden, 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 fehlschlagen, da sie die Methode implementieren sollte (entsprechend der Schnittstelle)

Bei meinen Unit-Tests versuche ich, so allgemein wie möglich zu sein: Wenn sich die Implementierung ändert, das sichtbare Verhalten (aus Sicht des Aufrufers) jedoch 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 zu testenden Funktion).

21
hoffmale

Der Hauptwert eines solchen Tests besteht darin, dass Ihre Implementierung umgestaltbar wird.

Ich habe in meiner Karriere viele Leistungsoptimierungen durchgeführt und häufig Probleme mit dem genauen Muster festgestellt, das Sie demonstriert haben: Um N Entitäten in der Datenbank zu speichern, führen Sie N Einfügungen durch. In der Regel ist es effizienter, eine Masseneinfügung mit einer einzigen Anweisung durchzuführen.

Andererseits wollen wir auch nicht vorzeitig optimieren. Wenn Sie normalerweise nur 1 bis 3 Personen gleichzeitig speichern, kann das Schreiben eines optimierten Stapels zu viel des Guten sein.

Mit einem geeigneten Komponententest können Sie ihn so schreiben, wie Sie ihn oben implementiert haben. Wenn Sie feststellen, dass Sie ihn optimieren müssen, können Sie dies mit dem Sicherheitsnetz eines automatisierten Tests tun, um Fehler zu erkennen. Dies hängt natürlich von der Qualität der Tests ab. Testen Sie also großzügig und gut.

Der sekundäre Vorteil beim Testen dieses Verhaltens durch Einheiten besteht darin, als Dokumentation für den Zweck zu dienen. Dieses triviale Beispiel mag offensichtlich sein, aber angesichts des nächsten Punktes unten könnte es sehr wichtig sein.

Der dritte Vorteil, auf den andere hingewiesen haben, besteht darin, dass Sie Details im Verborgenen testen können, die mit Integrations- oder Abnahmetests nur sehr schwer zu testen sind. Wenn beispielsweise alle Benutzer atomar gespeichert werden müssen, können Sie einen Testfall dafür schreiben, der Ihnen die Möglichkeit gibt, das erwartete Verhalten zu erkennen, und der auch als Dokumentation für eine Anforderung dient, die möglicherweise nicht offensichtlich ist an neue Entwickler.

Ich werde einen Gedanken hinzufügen, den ich von einem TDD-Lehrer bekommen habe. Testen Sie die Methode nicht. Testen Sie das Verhalten. Mit anderen Worten, Sie testen nicht, ob savePeople funktioniert, sondern, ob mehrere Benutzer in einem einzigen Aufruf gespeichert werden können.

Ich stellte fest, dass sich meine Fähigkeit, Qualitäts-Unit-Tests und TDD durchzuführen, verbesserte, als ich aufhörte, über Unit-Tests nachzudenken, um zu überprüfen, ob ein Programm funktioniert, sondern ob eine Code-Einheit was ich erwarte. Das ist anders. Sie überprüfen nicht, ob es funktioniert, aber sie überprüfen, ob es das tut, was ich denke. Als ich anfing so zu denken, änderte sich meine Perspektive.

13
Brandon

Sollte bakeCookies() getestet werden? Ja.

Wie sollte eine Funktion wie diese auf Einheit getestet werden, vorausgesetzt, Sie denken, dass dies der Fall sein sollte? Es fällt mir schwer, mir einen Unit-Test vorzustellen, bei dem Teig, Pfanne und Ofen nicht einfach verspottet werden und dann behauptet wird, dass Methoden für sie erforderlich sind.

Nicht wirklich. Schauen Sie sich genau an, WAS die Funktion tun soll - sie soll das Objekt oven auf einen bestimmten Zustand setzen. Wenn man sich den Code ansieht, scheint es, dass die Zustände der Objekte pan und dough nicht wirklich wichtig sind. Sie sollten also ein oven -Objekt übergeben (oder es verspotten) und am Ende des Funktionsaufrufs behaupten, dass es sich in einem bestimmten Zustand befindet.

Mit anderen Worten, Sie sollten behaupten, dass bakeCookies() die Cookies gebacken hat.

Bei sehr kurzen Funktionen scheinen Unit-Tests kaum mehr als Tautologie zu sein. Aber vergessen Sie nicht, Ihr Programm wird viel länger dauern als die Zeit, in der Sie es schreiben. Diese Funktion kann sich in Zukunft ändern oder auch nicht.

Unit-Tests erfüllen zwei Funktionen:

  1. Es testet, dass alles funktioniert. Dies ist der am wenigsten nützliche Funktionseinheitentest, und es scheint, dass Sie diese Funktionalität nur bei der Beantwortung der Frage berücksichtigen.

  2. Es wird überprüft, ob zukünftige Änderungen des Programms die zuvor implementierte Funktionalität nicht beeinträchtigen. Dies ist die nützlichste Funktion von Komponententests und verhindert die Einführung von Fehlern in große Programme. Es ist nützlich bei der normalen Codierung, wenn dem Programm Funktionen hinzugefügt werden, aber es ist nützlicher beim Refactoring und bei Optimierungen, bei denen die Kernalgorithmen, die das Programm implementieren, dramatisch geändert werden, ohne dass ein beobachtbares Verhalten des Programms geändert wird.

Testen Sie den Code nicht innerhalb der Funktion. Testen Sie stattdessen, ob die Funktion das tut, was sie verspricht. Wenn Sie Unit-Tests auf diese Weise betrachten (Testfunktionen, nicht Code), werden Sie feststellen, dass Sie niemals Sprachkonstrukte oder sogar Anwendungslogik testen. Sie testen eine API.

6
slebetman

Sollte savePeople () Unit-getestet werden, oder würden solche Tests dem Testen des integrierten forEach-Sprachkonstrukts gleichkommen?

Ja. Aber Sie könnten tun es auf eine Weise, die nur das Konstrukt erneut testen würde.

Hier ist zu beachten, wie sich diese Funktion verhält, wenn ein savePerson auf halbem Weg ausfällt. Wie soll es funktionieren?

That ist die Art von subtilem Verhalten, das die Funktion bietet und das Sie mit Unit-Tests erzwingen sollten.

3
Telastyn

Der Schlüssel hier ist Ihre Perspektive auf eine bestimmte Funktion als trivial. Der größte Teil der Programmierung ist trivial: Weisen Sie einen Wert zu, rechnen Sie nach, treffen Sie eine Entscheidung: Wenn dies dann der Fall ist, setzen Sie eine Schleife fort, bis ... Für sich genommen alles trivial. Sie haben gerade die ersten 5 Kapitel eines Buches durchgearbeitet, in dem eine Programmiersprache unterrichtet wird.

Die Tatsache, dass das Schreiben eines Tests so einfach ist, sollte ein Zeichen dafür sein, dass Ihr Design nicht so schlecht ist. Möchten Sie ein Design bevorzugen, das nicht einfach zu testen ist?

"Das wird sich nie ändern." So beginnen die meisten fehlgeschlagenen Projekte. Ein Komponententest stellt nur fest, ob das Gerät unter bestimmten Umständen wie erwartet funktioniert. Wenn Sie es bestehen lassen, können Sie die Implementierungsdetails vergessen und es einfach verwenden. Verwenden Sie diesen Gehirnraum für die nächste Aufgabe.

Zu wissen, dass die Dinge wie erwartet funktionieren, ist sehr wichtig und in großen Projekten und insbesondere in großen Teams nicht trivial. Wenn es eine Sache gibt, die Programmierer gemeinsam haben, ist die Tatsache, dass wir uns alle mit dem schrecklichen Code eines anderen befassen mussten. Das Mindeste, was wir tun können, sind einige Tests. Wenn Sie Zweifel haben, schreiben Sie einen Test und fahren Sie fort.

3
JeffO

Ich denke, Ihre Frage läuft darauf hinaus:

Wie teste ich eine Void-Funktion, ohne dass es sich um einen Integrationstest handelt?

Wenn wir beispielsweise Ihre Cookie-Backfunktion so ändern, dass Cookies zurückgegeben werden, wird sofort klar, wie der Test aussehen soll.

Wenn wir pan.GetCookies nach dem Aufruf der Funktion aufrufen müssen, können wir uns fragen, ob es sich wirklich um einen Integrationstest handelt oder nicht, aber testen wir nicht nur das pan-Objekt?

Ich denke, Sie haben Recht damit, dass Unit-Tests mit allem Verspotteten und nur das Überprüfen der Funktionen x y und z als Mangelwert bezeichnet wurden.

Aber! Ich würde argumentieren, dass Sie in diesem Fall Ihre void-Funktionen umgestalten sollten, um ein testbares Ergebnis zurückzugeben OR Verwenden Sie reale Objekte und führen Sie einen Integrationstest durch

--- Update für das Beispiel createNewUser

  • in der Datenbank muss ein neuer Benutzerdatensatz erstellt werden
  • eine Willkommens-E-Mail muss gesendet werden
  • die IP-Adresse des Benutzers muss zu Betrugszwecken aufgezeichnet werden.

OK, diesmal wird das Ergebnis der Funktion nicht einfach zurückgegeben. Wir wollen den Zustand der Parameter ändern.

Hier werde ich etwas kontrovers. Ich erstelle konkrete Scheinimplementierungen für die Stateful-Parameter

bitte, liebe Leser, versuchen Sie, Ihre Wut zu kontrollieren!

damit...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Dies trennt das Implementierungsdetail der zu testenden Methode vom gewünschten Verhalten. Eine alternative Implementierung:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Besteht den Unit-Test weiterhin. Außerdem haben Sie den Vorteil, dass Sie die Scheinobjekte testübergreifend wiederverwenden und in Ihre Anwendung für UI- oder Integrationstests einfügen können.

1
Ewan

Sollte savePeople () Unit-getestet werden, oder würden solche Tests dem Testen des integrierten forEach-Sprachkonstrukts gleichkommen?

Dies wurde bereits von @BryanOakley beantwortet, aber ich habe einige zusätzliche Argumente (denke ich):

Zunächst dient ein Komponententest zum Testen der Vertragserfüllung, nicht zum Implementieren einer API. Der Test sollte die Voraussetzungen festlegen, dann aufrufen und dann auf Effekte, Nebenwirkungen, Invarianten und Post-Bedingungen prüfen. Wenn Sie entscheiden, was getestet werden soll, die Implementierung der API spielt keine Rolle (und sollte es auch nicht).

Zweitens wird Ihr Test dazu dienen, die Invarianten zu überprüfen, wenn sich die Funktion ändert . Die Tatsache, dass es sich jetzt nicht ändert , bedeutet nicht, dass Sie den Test nicht haben sollten.

Drittens ist es sinnvoll, einen trivialen Test sowohl in einem TDD-Ansatz (der ihn vorschreibt) als auch außerhalb davon durchzuführen.

Wenn ich C++ für meine Klassen schreibe, neige ich dazu, einen trivialen Test zu schreiben, der ein Objekt instanziiert und Invarianten (zuweisbar, regulär usw.) überprüft. Ich fand es überraschend, wie oft dieser Test während der Entwicklung unterbrochen wurde (zum Beispiel durch versehentliches Hinzufügen eines nicht beweglichen Elements in einer Klasse).

1
utnapistim

Sie sollten auch bakeCookies testen - was würde/sollte z. B. bakeCookies(Egg, pan, oven) ergeben? Spiegelei oder eine Ausnahme? Für sich genommen kümmern sich weder pan noch oven um die tatsächlichen Zutaten, da keiner von ihnen dies tun soll, aber bakeCookies sollte normalerweise Kekse ergeben. Allgemeiner kann es davon abhängen, wie dough erhalten wird und ob ist eine Chance besteht, dass es nur Egg wird oder z. water stattdessen.

0
Tobias Kienzler