Eine gute Sammlung von Unit-Tests gibt Ihnen ein sicheres und freies Gefühl beim Refactoring, aber eine schlechte Sammlung von Tests kann Ihnen Angst vor dem Refactoring machen. Wie kommt das? Eine einzige Änderung am Anwendungscode kann eine Kaskade von fehlgeschlagenen Tests auslösen. Hier sind einige Tipps, wie Sie diese Situation vermeiden (oder bekämpfen) können.
Tipp 1: Testen Sie das Verhalten, nicht die Struktur
Das Verhalten des Systems ist das, worauf das Unternehmen Wert legt, und das sollten auch Sie aus Sicht der Verifizierung beachten. Wenn sich die Anforderungen drastisch ändern, sind auch Änderungen am System zu erwarten, auch an den Tests. Das Versprechen einer guten Abdeckung durch Unit-Tests besteht darin, dass Sie mit der Gewissheit refaktorisieren können, dass Ihre Tests alle Regressionen im Verhalten abfangen werden. Wenn Sie jedoch die Struktur Ihrer Anwendung und nicht das Verhalten testen, wird das Refactoring schwierig, da Sie die Struktur Ihres Codes ändern wollen, Ihre Tests diese Struktur aber bestätigen! Schlimmer noch, Ihre Testsuite testet vielleicht nicht einmal das Verhalten, aber Sie haben aufgrund der schieren Menge an strukturellen Tests Vertrauen in sie.
Wenn Sie das Verhalten des Systems von außen testen, können Sie die Implementierung frei ändern und Ihre Tests bleiben gültig. Ich spreche hier nicht unbedingt von Integrationstests, sondern von echten Unit-Tests, deren Einstiegspunkt eine natürliche Grenze ist. Bei der Arbeit haben wir Use-Case-Klassen, die diesen natürlichen Einstiegspunkt in jede Funktionalität bilden.
Schauen wir uns also ein Beispiel für strukturelle Tests an und sehen wir uns an, was passiert, wenn wir versuchen, eine Änderung an den Implementierungsdetails vorzunehmen. Als Beispiel haben wir einen Test gegen einen CreatePerson Anwendungsfall, der eine Person Klasse erstellt und sie persistiert, wenn es sich um ein gültiges Personenobjekt handelt. Der ursprüngliche Entwurf nimmt eine IValidator auf, um festzustellen, ob die Person gültig ist.
Beachten Sie, dass wir gegen eine Abhängigkeit (IValidator) des Anwendungsfalls (CreatePerson) testen. Unser Test hat strukturelles Wissen darüber, wie
Ihr Team hat versucht, einige neue Praktiken wie Domain-Driven Design einzuführen. Das Team hat darüber diskutiert und die Klasse Person stellt einen einfachen Einstieg dar. Sie haben die Aufgabe, das Verhalten in die Entität Person zu bringen und sie weniger blutarm zu machen.
Als ersten Versuch verschieben Sie die Validierungslogik in die Klasse Person.
Wenn wir uns den Anwendungsfall ansehen, brauchen wir IValidator nicht mehr zu injizieren. Wir müssen nicht nur ändern, was wir testen, sondern auch den Test komplett ändern, weil wir keinen Validator mehr haben, den wir als Attrappe einfügen können. Wir haben die ersten Anzeichen dafür gesehen, dass unsere Tests anfällig sind.
Versuchen wir, unseren Test auf das von uns erwartete Verhalten auszurichten, anstatt uns auf die Struktur unseres Codes zu verlassen.
Machen Sie sich vorerst nicht zu viele Gedanken über InMemoryPersonRepository people = Given.People;, wir werden darauf zurückkommen. Alles, was Sie wissen müssen, ist, dass InMemoryPersonRepository IPersonRepository implementiert.
Da wir IValidator und seine Implementierung nicht mehr benötigen, löschen wir sie. Wir können auch den Test CreatingPerson_WithValidPerson_CallsIsValid löschen, da wir jetzt einen besseren Test haben CreatePerson_WithValidName_PersistsPerson, der das Verhalten bestätigt, das uns interessiert, nämlich den Anwendungsfall, bei dem eine neue Person erstellt und persistiert wird. Juhu, weniger Testcode, bessere Abdeckung!
An dieser Stelle werden Sie vielleicht sagen: "Moment mal, Unit-Tests sollen doch nur eine Methode einer Klasse testen". Nein! Eine Unit ist alles, was Sie brauchen. Ich will damit keineswegs sagen, dass Sie keine Tests für Ihre kleinen Implementierungsdetails schreiben sollen, aber stellen Sie sicher, dass Sie sie problemlos löschen können, wenn sich etwas ändert. Da wir uns auf Verhaltenstests konzentrieren, können wir diese detaillierten Tests problemlos löschen und sind trotzdem abgesichert. Tatsächlich lösche ich die Tests oft einfach, nachdem ich mit der Entwicklung der Komponente fertig bin, da ich TDD nur für eine schnelle Feedbackschleife zum Design und zur Implementierung verwendet habe. Denken Sie daran, dass Testcode immer noch Code ist, der gewartet werden muss - je mehr Abdeckung für weniger, desto besser.
Also zurück zum Code. Wie sieht unser Anwendungsfall jetzt aus?
Das ist in Ordnung. Wir sind eine Abhängigkeit losgeworden und haben einige Logik in unsere Person Entität verschoben, aber wir können es besser machen. Bei der Durchsicht Ihres Pull Requests hat jemand aus dem Team auf etwas Wichtiges hingewiesen. Sie sollten darauf abzielen, nicht darstellbare Zustände nicht darstellbar zu machen. Das Geschäft erlaubt es nicht, eine Person ohne Namen zu speichern, also lassen Sie uns dafür sorgen, dass wir keine ungültige Person erstellen können.
Sehen Sie sich das an! Wir haben die Implementierung überarbeitet, ohne unseren Test aktualisieren zu müssen. Er funktioniert immer noch ohne jede Änderung.
Dies war ein erfundenes Beispiel, um den Punkt zu illustrieren, aber ich hoffe, dieser Tipp hilft Ihnen, besser wartbare Tests zu schreiben.
Tipp 2: Verwenden Sie In-Memory-Abhängigkeiten
Sie haben InMemoryPersonRepository bereits kennengelernt, daher sollte dieser Tipp weniger ausführlich sein. Die Behauptung ist einfach, dass die Wartbarkeit Ihrer Tests verbessert werden kann, wenn Sie mehr In-Memory-Versionen Ihrer Abhängigkeiten verwenden und weniger Mocking-Frameworks einsetzen.
Ich finde In-Memory-Versionen eines Repositorys, das mit einer Datenbank kommuniziert, aus mehreren Gründen besser als Mocking-Frameworks:
- Sie sind in der Regel einfacher zu aktualisieren als ein Mocking-Framework, insbesondere wenn die Mocks in jedem Test oder jeder Fixture erstellt werden.
- In Verbindung mit einigen Hilfsmitteln (siehe nächster Tipp) führen sie zu einer wesentlich einfacheren Einrichtung und Lesbarkeit
- Sie sind einfach zu verstehen
- Großartiges Werkzeug zur Fehlersuche
Der Nachteil ist, dass die Erstellung ein wenig Zeit in Anspruch nimmt.
Werfen wir einen kurzen Blick darauf, wie unser Code bisher aussieht:
Super einfach! Machen Sie sich die Mühe und probieren Sie es aus. Es ist vielleicht nicht so sexy wie ein Mocking-Framework, aber es wird Ihnen helfen, Ihre Testsuite besser zu verwalten.
Tipp 3: Aufbau von Testwerkzeugen
Test-Tooling bedeutet in diesem Zusammenhang Hilfsklassen, die die Lesbarkeit und Wartbarkeit der Tests erleichtern. Dabei geht es vor allem darum, die Tests übersichtlich zu gestalten und sie dennoch übersichtlich zu halten.
Lassen Sie uns ein paar Helfer besprechen, die Sie bei jedem Projekt haben sollten...
In-Memory-Abhängigkeiten
Dies wurde bereits oben besprochen. Ich kann gar nicht genug betonen, wie sehr dies die Wartung verbessert und das Nachdenken über Tests vereinfacht.
Bauherren
Builder können als einfache Methode zur Einrichtung von Testdaten verwendet werden. Sie sind eine großartige Möglichkeit, gleichzeitig Dutzende verschiedener Einrichtungsmethoden für Ihre Tests zu vermeiden und deutlich zu machen, wie die tatsächliche Einrichtung Ihres Tests aussieht, ohne in eine Einrichtungsmethode abzutauchen, die aussieht wie alle anderen Einrichtungsmethoden.
Ein kleiner Trick besteht darin, die Klasse, die Sie aufbauen, mit einer implicit Konvertierung zu versehen. Werfen Sie auch einen Blick auf Fluency, das Ihnen bei der Erstellung von Buildern hilft.
Eine letzte Anmerkung zu diesem Punkt. Nur weil ich viel mit Buildern arbeite, heißt das nicht, dass ich Mocking-Frameworks komplett aus dem Fenster werfe. Ich verwende Mocking-Frameworks nur für Dinge, die mir wirklich egal sind und die ich wahrscheinlich nicht ändern werde. Außerdem verwende ich sie eher in anderen Buildern als direkt in Tests. So haben Sie viel mehr Kontrolle über die Grammatik, die Sie für die Einrichtung Ihrer Tests verwenden.
Accessors
Ich bin mir nicht sicher, wie ich sie sonst nennen soll, aber es ist nützlich, eine statische Klasse zu haben, die den Zugriff auf Builder und andere Typen, die Sie bei der Einrichtung verwenden, vereinfacht. Normalerweise habe ich Given und A.
Dies ermöglicht es mir, einen sehr prägnanten Einrichtungscode zu schreiben. Wenn ich z.B. mein Personen-Repository mit 3 zufälligen Personen auffüllen möchte, könnte ich das so machen:
Der Vollständigkeit halber die PersonBuilder Implementierung:
Einpacken
Das sind also meine 3 Tipps für eine bessere Wartbarkeit Ihrer Tests. Ich möchte Sie ermutigen, sie auszuprobieren. Wenn Sie nicht in die Wartbarkeit Ihrer Tests investieren, können diese schnell zu einer Belastung statt zu einem Segen werden. Ich habe gesehen, dass die oben genannten Praktiken nicht nur in meinen Teams zu Verbesserungen geführt haben, sondern auch andere Kollegen sind zu ähnlichen Erkenntnissen gelangt und haben die gleichen positiven Ergebnisse erzielt. Lassen Sie mich wissen, ob Sie dies hilfreich finden oder ob es Punkte gibt, mit denen Sie nicht einverstanden sind. Ich würde mich über eine Diskussion in den Kommentaren freuen. Viel Spaß beim Codieren!
Wenn Ihnen dieser Artikel gefallen hat, könnten Ihnen auch einige meiner anderen Artikel zum Thema Testen gefallen:
- Anatomie einer automatisierten Testsuite
- Schreiben von lesbaren Akzeptanztests
- Überprüfung: F# Unit Testing Frameworks und Bibliotheken
- Das Seitenmodulmodell mit F# und Canopy
Wir befähigen Unternehmen, zuverlässige und hochwertige Software zu liefern. Haben Sie Fragen? Wir sind für Sie da! www.qxperts.io
Verfasst von
Devon Burriss
Unsere Ideen
Weitere Blogs
Contact




