Vor- und Nachteile bei der Erstellung eines gemeinsamen Gerüstcodes für Unit-Tests

8

Für das Projekt, an dem mein Team und ich arbeiten, stellen wir häufig fest, dass wir große Teile des Gerüstcodes benötigen. Das Erstellen von Domänenobjekten mit korrekten Werten, das Einrichten von Mocks für Repositorys, der Umgang mit dem Cache usw. sind alles Dinge, die während der Tests häufig vorkommen. Oft arbeiten wir mit denselben Basisobjekten, die für unsere Domäne von zentraler Bedeutung sind (Person, ...), sodass bei vielen Tests Instanzen dieser Objekte erstellt werden, an denen andere Objekte arbeiten können. Wir haben viele verschiedene Lösungen, die die Basisdomäne verwenden, daher wird diese Art von Code häufig auf diese Lösungen verteilt.

Ich habe darüber nachgedacht, gemeinsame Klassen zu schaffen, die einen Großteil dieses Gerüsts übernehmen. Dies würde es uns ermöglichen, eine vollständig instanziierte Person mit allem, was eingerichtet ist (Zugriff über Repository, Caching ...), anzufordern. Dies entfernt doppelten Code aus unseren einzelnen Komponententests, würde aber auch bedeuten, dass es eine große Menge an Code gibt, die wahrscheinlich "zu viel" pro Test ausführt (da dies ein vollständiges Objekt und nicht nur die erforderlichen Teile einrichten würde).

Hat das jemals jemand gemacht? Gibt es Erkenntnisse, Bemerkungen, Gedanken ..., die Sie anbieten können, um diesen Ansatz zu bestätigen oder ungültig zu machen?

JDT
quelle
Ich habe Ihre Frage leicht bearbeitet, bevor ich sie hierher migrierte, um Dinge zu entfernen, die normalerweise in eine Antwort eingehen würden. Wenn Sie Vor- und Nachteile in der Frage selbst auflisten, verringert sich die Möglichkeit für jemanden, eine umfassende Antwort zu geben, die nicht das dupliziert, was Sie bereits gesagt haben. Auf diese Weise werden die von Ihnen gemachten Punkte idealerweise als Teil der Antworten hervorgehoben. Wenn dies nicht der Fall ist, können Sie sie als Antwort erneut veröffentlichen. Wenn Sie in der Community darüber abstimmen, können Sie feststellen, ob Sie mit dem Geld richtig liegen oder nicht.
Adam Lear

Antworten:

4
Creating domain objects with correct values

Ich verwende das Builder-Muster, um Domänenobjekte für Tests zu erstellen, wie im Buch " Wachsende objektorientierte Software " beschrieben. Die Builder verfügen über statische Methoden, die Standardwerte für allgemeine Objekte mit Methoden zum Überschreiben der Standardwerte generieren.

User user = UserBuilder.anAdminUser().withEmail("[email protected]").build();

Sie können diese kombinieren, um komplexere Objekte zu generieren.

Wir verwenden auch die Vererbung von Testfallklassen mit häufig verwendeten Mocks für bestimmte Arten von Komponententests. Beispielsweise erfordern MVC-Controller-Tests häufig dieselben Mocks / Stubs für Anforderungs- und Antwortobjekte.

leer
quelle
2

Das Einrichten für alle Tests kann eine gute Idee sein, insbesondere wenn Sie eine Bereitstellung durchführen. Anstatt Mocks zu verwenden, erstellen, implementieren und füllen wir eine Testdatenbank und richten den Test dann auf die Instanz. Nicht genau so, wie Sie es gemäß dem Handbuch tun sollten, aber es funktioniert, umso mehr, als wir unseren Bereitstellungscode verwenden, um dies zu tun.

Tony Hopkinson
quelle
Nehmen wir an, dass es überhaupt keine Abhängigkeit von der Datenbank gibt und die Objekte POCOs sind. Würden Sie trotzdem eine Datenbank in Betracht ziehen, aus der sie abgerufen werden können? Würde sich die Pflege einer Datenbank, die nur zum Testen verwendet wird, von der Pflege des Codes zum Erstellen von Objekten unterscheiden, die nur zum Testen verwendet werden?
JDT
2

Bedwyr Humphreys spricht von einer Builder-Lösung, die ich einige Male verwendet habe und die wirklich gut funktioniert. Fast keine Codeduplizierung und keine saubere Erstellung von Domänenobjekten.

User user = UserBuilder.anAdminUser().withEmail("[email protected]").build();

Ich bin kein Fan der Codegenerierung (Gerüste, Codesmith, lblgen, ..). Es hilft Ihnen nur, doppelten Code viel schneller und nicht wirklich wartungsfreundlich zu schreiben. Wenn ein Framework dazu führt, dass Sie viel doppelten (Test-) Code schreiben, müssen Sie möglicherweise einen Weg finden, um die Codebasis vom Framework zu entfernen.

Wenn Sie weiterhin Repository-Zugangscode schreiben, können Sie dies möglicherweise mit einem generischen Basis-Repository lösen und über eine generische Basis-Testklasse verfügen, von der Ihre Komponententests erben.

Um Codeduplizierungen zu vermeiden, kann AOP auch bei der Protokollierung, Zwischenspeicherung, ...

Pascal Mestdach
quelle
1

Möglicherweise möchten Sie das Design des zu testenden Codes überdenken. Vielleicht braucht es einige Beispiele für das Fassadenmuster? Ihr Testcode könnte die Fassaden verwenden und wäre einfacher.


quelle
Es ist ein gültiger Ansatz - aber nicht für die Codebasis, die wir haben. Ich arbeite derzeit mit Builder-Klassen, die XML-basierte Nachrichten aus unseren Domänenobjekten erstellen. Keine Anzahl von Fassadenklassen hindert mich daran, irgendwann vollständig konstruierte Objekte einzustecken - etwas, das ich in verschiedenen Lösungen mehrmals tun musste.
JDT
1

Ich kann mich auf diese Frage beziehen. In meinem Team haben wir schon früher versucht, TDD zu machen, und es war die gleiche Geschichte. Zu viele Tests erforderten viele Gerüst- / Dienstprogrammmethoden, die wir einfach nicht hatten. Aufgrund des Zeitdrucks und einiger anderer nicht so richtiger Entscheidungen endeten wir mit Unit-Tests, die sehr lang waren und die Dinge neu erfanden. Im Laufe einer Veröffentlichung wurden diese Unit-Tests so kompliziert und schwer zu warten, dass wir sie fallen lassen mussten.

Ich arbeite derzeit mit wenigen anderen Entwicklern an der Wiedereinführung von TDD in das Team, aber anstatt den Leuten einfach zu sagen, Tests zu schreiben, möchten wir diesmal vorsichtig vorgehen und die Leute haben die richtigen Fähigkeiten und die richtigen Werkzeuge. Das Fehlen des allgemeinen Support-Codes ist eine Sache, die wir identifiziert haben, wo wir uns verbessern müssen, damit der gesamte Code, den sie schreiben, klein und auf den Punkt kommt, wenn andere anfangen, Tests zu schreiben.

Ich habe noch nicht so viel Erfahrung in diesem Bereich, wie ich möchte, aber einige Dinge, die ich vorschlagen würde, basierend auf unserer bisherigen Arbeit und einigen Hausaufgaben, die ich gemacht habe:

  1. Gemeinsamer Code ist eine gute Sache. Die gleiche Arbeit 5 Mal zu machen macht keinen Sinn und unter Zeitdruck werden mindestens 4 dieser Zeiten zur Hälfte bewertet.
  2. Mein Plan ist es, ein gemeinsames Framework aufzubauen und dann ein TDD-Führungsteam aufzubauen, das aus 4-6 Entwicklern besteht. Lassen Sie dann Unit-Tests schreiben, das Framework kennenlernen und Feedback dazu geben. Lassen Sie diese Leute dann zum Rest der Gruppe gehen und andere Leute bei der Erstellung wartbarer Komponententests unterstützen, die die Leistung des Frameworks nutzen. Lassen Sie diese Leute auch Feedback sammeln und es wieder in das Framework einbringen.
  3. Für die Beispiele, die Sie mit der Person-Klasse bereitgestellt haben, untersuche ich tatsächlich die Verwendung von Mockpp- oder Googlemock- Frameworks. Ich neige mich vorerst zu Googlemock, habe es aber auch noch nicht versucht. Warum sollte man in beiden Fällen etwas schreiben, das "ausgeliehen" werden kann?
  4. Seien Sie vorsichtig beim Erstellen von "Gott" -Einrichtungen, die Objekte für alle Tests instanziieren, unabhängig davon, ob diese Tests diese Objekte tatsächlich benötigen. Dies nennt xUnit Test Patterns "General Fixture Pattern". Dies kann zu wenigen Problemen führen, z. B. a) dass die Ausführung jedes Tests aufgrund übermäßigen Setup-Codes zu lange dauert und b) Tests zu viele unnötige Abhängigkeiten aufweisen. In unserem Framework wählt jede Testklasse genau die Framework-Funktionen aus, die für die Ausführung benötigt werden, sodass kein unnötiges Gepäck enthalten ist (in Zukunft können wir es für jede Methode einzeln ändern). Ich habe mich von Boost und Modern C ++ Design inspirieren lassenund entwickelte eine Basisvorlagenklasse, die einen mpl :: -Vektor der benötigten Features verwendet und eine benutzerdefinierte Klassenhierarchie speziell für diesen Test erstellt. Einige wirklich raffinierte Captain Cruch Decoder Ring super verrückte schwarze Magie Vorlage Zeug :)
DXM
quelle
1

"Jedes Problem in der Informatik kann mit einer zusätzlichen Indirektionsebene gelöst werden. Aber das schafft normalerweise ein anderes Problem." - David Wheeler

(Haftungsausschluss: Ich bin Teil des JDT-Teams, daher habe ich mehr inneres Wissen über das hier vorgestellte Problem.)

Das wesentliche Problem hierbei ist, dass die Objekte im Framework selbst eine Datenbankverbindung erwarten, da diese lose auf dem CSLA-Framework von Rockford Lhotka basiert. Dies macht es fast unmöglich, sie zu verspotten (da Sie das Framework ändern müssten, um dies zu unterstützen) und verstößt auch gegen das Black-Box-Prinzip, das ein Unit-Test haben sollte.

Ihre vorgeschlagene Lösung ist zwar an sich keine schlechte Idee, erfordert jedoch nicht nur eine Menge Arbeit, um zu einer stabilen Lösung zu gelangen, sondern fügt auch eine weitere Ebene von Klassen hinzu, die gewartet und erweitert werden müssen, wodurch eine bereits vorhandene noch komplexer wird komplexe Situation.

Ich stimme zu, dass Sie immer nach der besten Lösung suchen sollten, aber in einer realen Situation wie dieser hat das Praktische auch seine Werte.

Und die Praktikabilität schlägt die folgende Option vor:

Verwenden Sie eine Datenbank.

Ja, ich weiß, dass es technisch gesehen kein "echter" Unit-Test mehr ist, aber genau genommen ist es auch kein realer Unit-Test mehr, sobald Sie in Ihrem Test eine Klassengrenze überschreiten. Was wir uns hier fragen müssen, ist, ob wir "reine" Komponententests wollen, die eine Menge Gerüst- und Klebercode erfordern, oder ob wir testbaren Code wollen ?

Sie können auch die Tatsache nutzen, dass das verwendete Framework den gesamten Datenbankzugriff für Sie kapselt, und Ihre Tests für eine saubere Sandbox-Datenbank ausführen. Wenn Sie dann jeden Test in einer eigenen Transaktion ausführen und am Ende jedes 'logischen' Satzes von Tests einen Rollback durchführen, sollten Sie keine Probleme mit der gegenseitigen Abhängigkeit oder der Testreihenfolge haben.

Versuchen Sie nicht, eine Datenbank aus der Datenbank zu entfernen.

Sam
quelle
Sam, siehe meinen Kommentar zu Tony Hopkinsons Antwort. Würden Sie dasselbe tun, wenn es sich um ein Projekt handelt, das nur aus POCO-Klassen besteht?
JDT
Es sei denn, Sie testen einen Algorithmus, der ohne externe Ressourcen für sich allein lebt. Die Idee wäre, dass Ihre 'Test'-Datenbank nur minimale Daten enthält und dass alle nicht infrastrukturellen Testdaten von den Tests selbst eingefügt werden müssten, wodurch Ihre Wartbarkeitskosten reduziert würden. Zugegebenermaßen ist bei 'klaren' POCO-Objekten die Argumentation für eine Testdatenbank nicht so stark.
Sam
0

Ich würde davor warnen, zu viel Code zu haben, der zu viel "schwarze Magie" bewirkt, ohne dass Entwickler wissen, was genau eingerichtet wird. Mit der Zeit werden Sie auch sehr abhängig von diesem Code / diesen Daten. Deshalb bin ich kein Fan davon, Ihre Tests auf Testdatenbanken zu stützen. Ich glaube, Testdatenbanken dienen zum Testen als Benutzer, nicht für automatisierte Tests.

Das heißt, Ihr Problem ist real. Ich würde einige gängige Methoden entwickeln, um Daten einzurichten, die Sie regelmäßig benötigen. Versuchen Sie im Idealfall, die Methoden zu parametrisieren, da bestimmte Tests unterschiedliche Daten benötigen.

Aber ich würde es vermeiden, große Objektgraphen zu erstellen. Am besten lassen Sie Tests mit dem Minimum an Daten ausführen, die sie benötigen. Wenn viele andere Daten vorhanden sind, können Sie Ihre Testergebnisse unerwartet beeinflussen (Worst-Case-Szenario).

Ich mag es, meine Tests so "silo'ed" wie möglich zu halten. Es ist jedoch eine subjektive Frage. Aber ich würde mich für gängige Methoden mit Parametern entscheiden, damit Sie Ihre Testdaten in nur wenigen Codezeilen einrichten können.

Peter
quelle
0

Alle Möglichkeiten in den Antworten zusammengefasst:

Verwenden Sie das Builder-Muster (Bedwyr Humphreys, DXM und Pascal Mestdach).

Vorteile:

  • Keine Codeduplizierung in Tests
  • Kann verschiedene Builder für komplexe Tests kombinieren
  • Schnittstelle reinigen
  • Große Objektgraphen von Buildern können das Testergebnis beeinflussen

Nachteile:

  • Bauherren müssen geschrieben und gepflegt werden
  • Der Builder-Code muss ebenfalls getestet werden
  • Builder-Code erfordert ständige Pflege und Aufmerksamkeit

Verwenden Sie eine Testdatenbank (Tony Hopkinson und Sam)

Vorteile:

  • Einfachheit
  • Alle Daten an einem zentralen Ort
  • Kann bereits entwickelten Datenzugriffscode verwenden
  • Verwendbar für Domänenobjekte, die nicht POCO sind (z. B. Active Record, ...)

Nachteile:

  • Erfordert die Verwendung des DAL, der die Tests beeinträchtigen kann
  • Nicht "reines" TDD
  • 'Black Box', muss einen Blick in die Datenbank werfen, um zu sehen, welche Daten Ihre Objekte enthalten

Erstellen Sie Domänenobjekte pro Test (Peter)

Vorteile:

  • Isolierter Testcode
  • Keine 'Black Box', was Sie im Testcode sehen, bekommen Sie

Nachteile:

  • Codeduplizierung für allgemeine Objekte

Verwenden Sie Fassaden für Domänenobjekte (Raedwald)

Vorteile:

  • Einfache Tests

Nachteile:

  • Nicht in allen Fällen verwendbar
  • Der hinter Facade versteckte Code muss ebenfalls getestet werden
JDT
quelle