Unit-Test einer Klasse, die DI ohne Prüfung auf Interna verwendet

12

Ich habe eine Klasse, die in 1 Hauptklasse und 2 kleineren Klassen überarbeitet wird. Die Hauptklassen verwenden die Datenbank (wie viele meiner Klassen) und senden eine E-Mail. Also hat die Hauptklasse ein IPersonRepositoryund ein IEmailRepositorygespritzt was seinerseits an die 2 kleineren Klassen schickt.

Jetzt möchte ich die Hauptklasse einem Unit-Test unterziehen und habe gelernt, die internen Abläufe der Klasse nicht zu zerstören, da wir in der Lage sein sollten, die internen Abläufe zu ändern, ohne die Unit-Tests zu unterbrechen.

Aber da die Klasse die verwendet IPersonRepositoryund eine IEmailRepository, ich HABE angeben (Mock / Dummy) Ergebnisse für einige Methoden für die IPersonRepository. Die Hauptklasse berechnet einige Daten basierend auf vorhandenen Daten und gibt diese zurück. Wenn ich das testen möchte, sehe ich nicht, wie ich einen Test schreiben kann, ohne anzugeben, dass das IPersonRepository.GetSavingsByCustomerIdx zurückgibt. Aber dann "weiß" mein Unit-Test über die internen Abläufe Bescheid, weil er "weiß", welche Methoden zu verspotten sind und welche nicht.

Wie kann ich eine Klasse testen, die Abhängigkeiten eingeführt hat, ohne dass der Test über die Interna Bescheid weiß?

Hintergrund:

Nach meiner Erfahrung erstellen viele Tests wie diese Mocks für die Repositorys und stellen dann entweder die richtigen Daten für die Mocks bereit oder testen, ob während der Ausführung eine bestimmte Methode aufgerufen wurde. In jedem Fall kennt der Test die Interna.

Jetzt habe ich eine Präsentation über die Theorie gesehen (die ich zuvor gehört habe), dass der Test nichts über die Implementierung wissen sollte. Erstens, weil Sie nicht testen, wie es funktioniert, sondern auch, weil, wenn Sie jetzt die Implementierung ändern, alle Komponententests fehlschlagen, weil sie über die Implementierung Bescheid wissen. Obwohl mir das Konzept der Tests nicht bekannt ist, weiß ich nicht, wie ich es durchführen soll.

Michel
quelle
1
Sobald Ihre Hauptklasse ein IPersonRepositoryObjekt erwartet , sind diese Schnittstelle und alle darin beschriebenen Methoden nicht mehr "intern", sodass es sich nicht wirklich um ein Testproblem handelt. Ihre eigentliche Frage sollte lauten: "Wie kann ich Klassen in kleinere Einheiten umgestalten, ohne zu viel in der Öffentlichkeit preiszugeben?" Die Antwort lautet "Halten Sie diese Schnittstellen schlank" (indem Sie sich beispielsweise an das Prinzip der Schnittstellenseggregation halten). Das ist meiner Meinung nach Punkt 2 in der Antwort von @ DavidArno (ich glaube, ich muss das nicht in einer anderen Antwort wiederholen).
Doc Brown

Antworten:

7

Die Hauptklasse berechnet einige Daten basierend auf vorhandenen Daten und gibt diese zurück. Wenn ich das testen möchte, sehe ich nicht, wie ich einen Test schreiben kann, ohne anzugeben, dass das IPersonRepository.GetSavingsByCustomerIdx zurückgibt. Aber dann "weiß" mein Unit-Test über die internen Abläufe Bescheid, weil er "weiß", welche Methoden zu verspotten sind und welche nicht.

Sie haben Recht, dass dies eine Verletzung des Prinzips "Interna nicht testen" ist und häufig übersehen wird.

Wie kann ich eine Klasse testen, die Abhängigkeiten eingeführt hat, ohne dass der Test über die Interna Bescheid weiß?

Es gibt zwei Lösungen, mit denen Sie diesen Verstoß umgehen können:

1) Geben Sie ein vollständiges Modell von an IPersonRepository. Ihr derzeit beschriebener Ansatz besteht darin, den Mock an die inneren Abläufe der zu testenden Methode zu koppeln, indem Sie nur die aufgerufenen Methoden verspotten. Wenn Sie Mocks für alle Methoden von IPersonRepositoryangeben, entfernen Sie diese Kupplung. Das Innenleben kann sich ändern, ohne den Schein zu beeinträchtigen, wodurch der Test weniger spröde wird.

Dieser Ansatz hat den Vorteil, dass der DI-Mechanismus einfach gehalten wird. Er kann jedoch viel Arbeit verursachen, wenn Ihre Schnittstelle viele Methoden definiert.

2) Spritzen Sie IPersonRepositoryweder die GetSavingsByCustomerIdMethode noch einen Einsparungswert ein. Das Problem beim Injizieren ganzer Schnittstellenimplementierungen besteht darin, dass Sie dann ein "Ask, Don't Tell" -System injizieren ("Tell, Don't Ask") und die beiden Ansätze vertauschen. Wenn Sie einen "reinen DI" -Ansatz wählen, sollte der Methode die genaue aufzurufende Methode bereitgestellt (mitgeteilt) werden, wenn ein Einsparungswert gewünscht wird, anstatt ein Objekt zu erhalten (das dann effektiv abgefragt werden muss, damit die Methode aufgerufen wird).

Der Vorteil dieses Ansatzes besteht darin, dass keine Verspottungen erforderlich sind (über die Testmethoden hinaus, die Sie in die zu testende Methode einfügen). Der Nachteil besteht darin, dass sich die Methodensignaturen ändern können, wenn sich die Anforderungen der Methode ändern.

Beide Ansätze haben ihre Vor- und Nachteile. Wählen Sie also den Ansatz, der Ihren Anforderungen am besten entspricht.

David Arno
quelle
1
Ich mag Idee Nummer 2 wirklich. Ich weiß nicht, warum ich das noch nie gedacht habe. Ich erstelle schon seit einigen Jahren Abhängigkeiten von IRepositories, ohne mich jemals zu fragen, warum ich eine Abhängigkeit von einem ganzen IRepository erstellt habe (das in vielen Fällen ziemlich viele Methodensignaturen enthält), während es nur eine Abhängigkeit von einem oder hat 2 Methoden. Anstatt also ein IRepository zu injizieren, könnte ich die benötigte (einzige) Methodensignatur besser injizieren oder weitergeben.
Michel
1

Mein Ansatz ist es, Scheinversionen der Repositorys zu erstellen, die aus einfachen Dateien mit den erforderlichen Daten gelesen werden.

Dies bedeutet, dass der einzelne Test nichts über das Setup des Mocks 'weiß', obwohl das gesamte Testprojekt offensichtlich auf das Mock verweist und die Setup-Dateien usw. enthält.

Dies vermeidet die komplexe Einrichtung von Scheinobjekten, die für das Verspotten von Frameworks erforderlich ist, und ermöglicht es Ihnen, die Scheinobjekte in realen Instanzen Ihrer Anwendung für UI-Tests und dergleichen zu verwenden.

Da der Mock vollständig implementiert ist und nicht speziell für Ihr Testszenario eingerichtet wurde, sollte bei einer Änderung der Implementierung beispielsweise GetSavingsForCustomer jetzt auch den Kunden löschen. Die Tests werden nicht abgebrochen (es sei denn, es werden die Tests wirklich abgebrochen). Sie müssen nur Ihre einzelne Mock-Implementierung aktualisieren, und alle Ihre Tests werden dagegen ausgeführt, ohne dass ihre Einrichtung geändert wird

Ewan
quelle
2
Dies führt immer noch dazu, dass ich ein Modell mit Daten für genau die Methoden erstelle, mit denen die getestete Klasse arbeitet. Daher habe ich immer noch das Problem, dass der Test unterbrochen wird, wenn die Implementierung geändert wird.
Michel
1
bearbeitet, um zu erklären, warum mein Ansatz besser ist, obwohl immer noch nicht perfekt für das Szenario, das ich meine
Ewan
1

Unit-Tests sind in der Regel Whitebox-Tests (Sie haben Zugriff auf den realen Code). Daher ist es in Ordnung, Interna bis zu einem gewissen Grad zu kennen, für Anfänger ist es jedoch einfacher, dies nicht zu tun, da Sie Interna nicht testen sollten Verhalten (z. B. "Methode a zuerst, dann b und dann a erneut aufrufen").

Das Injizieren eines Modells, das Daten bereitstellt, ist in Ordnung, da Ihre Klasse (Einheit) von diesen externen Daten (oder dem Datenanbieter) abhängt. Sie sollten jedoch die Ergebnisse überprüfen (nicht den Weg, um zu ihnen zu gelangen)! Sie stellen z. B. eine Personeninstanz bereit und stellen sicher, dass eine E-Mail an die richtige E-Mail-Adresse gesendet wurde, indem Sie z. B. auch einen Schein für die E-Mail bereitstellen. Dieser Schein speichert lediglich die E-Mail-Adresse des Empfängers für den späteren Zugriff durch Ihren Test -Code. (Ich glaube, Martin Fowler nennt sie eher Stubs als Mocks.)

Andy
quelle