13. Nov 2023
Lesedauer 18 Min.
So klappt es auch mit den Unit-Tests
30 Tipps für erfolgreiches Testen
Eine Sammlung von Best Practices aus über 15 Jahren Entwicklungserfahrung.

Code verändern und keine Angst haben müssen, etwas kaputt zu machen. Der Autor dieser Zeilen ist sich ziemlich sicher, dass sich das alle Entwickler wünschen. Doch dieser Wunsch wird nicht ohne Gegenleistung erfüllt. Sie brauchen Code, der leicht zu lesen ist, funktionale Benennungen verwendet, eine hohe Kohäsion aufweist und ausreichend vom Rest der Codebasis abgekoppelt ist. Aber das ist nicht genug.Sie brauchen auch Zugang zu einer Reihe von automatisierten Tests mit ausreichender Abdeckung, um sicher sein zu können, dass Sie nichts kaputt gemacht haben. Und zwar nicht nur irgendwelche automatisierten Tests, sondern Tests, die klare Absichten verfolgen, in Millisekunden abgeschlossen sind, parallel ausgeführt werden können und eine selbstbeschreibende Fehlermeldung liefern, die dafür sorgt, dass der Debugger nicht in Aktion treten muss.Ich kann Ihnen sagen, dass dies das Versprechen ist, das die testgetriebene Entwicklung einlösen kann – wenn man sie richtig einsetzt. Da das aber nicht so einfach ist, ist das Hauptziel dieses Artikels, Hinweise und Tipps zu geben, wie man es eben richtig macht.Es gibt sicher viele Leute, die Ihnen sagen werden, dass die Investitionen nichts wert sind oder dass es nur die Bereitstellung von Funktionalität bremsen wird. Obwohl ich weiß, dass diese Leute falsch liegen, kann man ihre Einstellung mit einem Blick auf den Gartner-Hype-Zyklus (Bild 1) erklären.

Der Gartner-Hype-Zyklus (Bild 1)
Autor
Der Gipfel der aufgeblähten Erwartungen („Peak of Inflated Expectations“) ist erreicht, wenn Sie hundertprozentig überzeugt sind, dass TDD genau der richtige Weg ist und versuchen, alle von dieser Tatsache zu überzeugen. Die Fratze der Trough of Disillusionment jedoch zeigt sich, wenn Sie die Kehrseite von TDD entdecken und beginnen, jedem – einschließlich sich selbst – zu sagen, dass er von TDD die Finger lassen soll.Den Hang der Erleuchtung erklimmen Sie, wenn Sie das Für und Wider gelernt haben. Und das Plateau der Produktivität ist der Zeitpunkt, an dem Sie alle dogmatischen Überzeugungen über Bord geworfen haben und schließlich ein geübter Praktiker von TDD geworden sind.Und vergessen Sie nicht, dass dies ein wiederkehrender Zyklus ist. Jedes Mal, wenn Sie übermäßig begeistert von einer Innovation oder Technologie sind, sollten Sie sich fragen, wo Sie stehen.Lassen Sie uns nun die Grundsätze, Praktiken und Heuristiken durchgehen, die ich selbst anwende, um diese dogmatischen Enden der Skala zu vermeiden.
Für Testbarkeit entwerfen
Verstehen Sie die internen Grenzen Ihres SystemsAuf einen Aspekt müssen Sie besonders achten: die internen Grenzen Ihres Systems. Ganz gleich ob Sie ein neues System aufbauen oder versuchen, ein bestehendes System besser testbar zu machen – dies ist ein entscheidender Teil davon. Diese Grenzen helfen Ihnen, den richtigen Umfang für Unit- und Integrationstests zu finden, die Notwendigkeit globaler Dependency-Injection-Container zu vermeiden und natürliche Nahtstellen zu schaffen, die Sie dabei unterstützen, zu viel Kopplung zu vermeiden.Stellen Sie sich also einige Fragen wie: Welche Schichten können Sie identifizieren? Wie ist der Architekturstil? Können Sie funktionale Abschnitte oder Komponenten erkennen? Weist jede Schicht denselben Architekturstil auf, oder gibt es Unterschiede zwischen den Schichten? Welche Eigenschaften können dupliziert werden, und welche davon müssen unbedingt sorgfältig geschützt werden? Ich habe schon erlebt, dass Teams ihre Codebasis in den Sand gesetzt haben, weil die Entwickler die Grenzen nicht verstanden hatten.Verantwortlichkeiten planen, bevor Sie erste Tests schreiben …Niemand, der bei klarem Verstand ist, sollte einen automatisierten Test aus heiterem Himmel schreiben. Sie sollten immer mit einer Skizze beginnen – entweder digital, gedanklich oder auf einem Stück Papier –, um herauszufinden, was ein Typ, eine Klasse oder ein Modul tun soll und wie sie mit anderen zusammenhängen. Dann schreiben Sie das Skelett in Code, und erst dann beginnen Sie mit dem Schreiben einiger Tests, um ein Gefühl für die API-Oberfläche zu bekommen und diese zur Verfeinerung der Aufgaben zu nutzen. Es kommt nicht selten vor, dass ich einen ganzen Tag damit verbringe, die erste Implementierung zu programmieren, und erst danach die Testbarkeit dieses Codes überdenke.… und dann das Verhalten und Ihr API mithilfe der Tests steuernIm Gegensatz zu dem, was viele Leute denken, ist die testgetriebene Entwicklung in Wirklichkeit ein Designprozess, bei dem die Tests Ihnen helfen sollen, die Oberfläche und das Verhalten Ihres API zu steuern. Aus diesem Grund können sie oft als Dokumentation dienen. Sie zeigen, wie das API verwendet werden soll und welches Verhalten man ausschließen sollte. In ähnlicher Weise ist ein Fehler nur ein Symptom für einen fehlenden Test. Deshalb korrigiere ich meine Tests immer mit Specs. Wenn Sie anfangen, Ihre Tests nach TDD zu schreiben, werden Sie feststellen, dass dies einen kreativen Denkprozess auslöst, bei dem Ihnen alternative Szenarien und Randfälle in den Sinn kommen.Vermeiden Sie technische Verzeichnisse Meiner Meinung nach sollten Sie sicherstellen, dass Sie Ihren Code in funktionalen Ordnern gruppieren, die auf seine funktionalen und technischen Fähigkeiten abgestimmt sind. Das ist der beste Weg, um ein großes System und seine Codebasis verständlicher zu machen. Dadurch wird klarer, was zusammengehört, wo die internen Grenzen liegen und was als internes Implementierungsdetail betrachtet werden sollte.Der größte Vorteil ist jedoch, dass diese internen Grenzen einen natürlichen Rahmen für automatisierte Tests bilden und zeigen, ob Mocking eingesetzt werden soll oder nicht. Aus diesem Grund sind benachbarte Ordner im Wesentlichen separate Module oder funktionale Fähigkeiten und sollten daher auch als solche behandelt werden. Sie bilden potenziell getrennte Bereiche für das Testen, die Anwendung von Don’t Repeat Yourself (DRY) und Dependency Injection.Es ist in Ordnung, konkrete Klassen zu injizieren Viele erfahrene Entwickler sind besessen von Abstraktionen. Sie benutzen Prinzipien wie SOLID als Vorwand, um jeder konkreten Klasse eine Schnittstelle zu verpassen, nur damit man Dependency Injection durchführen kann.Nun, vorausgesetzt, Sie haben Ihre internen Grenzen identifiziert und Ihren Testumfang daran ausgerichtet, ist es völlig in Ordnung, konkrete Klassen als Abhängigkeiten zu verwenden. Ich ziehe es sogar vor, die Verwendung eines Dependency Injection Frameworks bei einem Modul oder einer Komponente gänzlich zu vermeiden. Und wenn ich es doch tue, verwende ich einen lokalen Container.Verwenden Sie das DI-Prinzip, um mit Hässlichkeit umzugehenDas Problem der meisten Abstraktionen und Schnittstellen besteht darin, dass sie sehr allgemein gehalten und oft aufgebläht sind. Die Abhängigkeit von solchen Abstraktionen führt zu einer Kopplung, die Sie nicht wollen. Ich kenne zwei sehr leistungsfähige Techniken, um sich davon zu lösen. Die eine besteht in der Verwendung eines benannten Delegaten und die andere in der Umkehrung der Abhängigkeit (auch bekannt als das Dependency Inversion Principle).Dies funktioniert, indem die konsumierende Partei ihre eigene Schnittstelle definiert und einen Adapter verwendet, um Aufrufe von der neuen Abstraktion zur alten zu überbrücken. Das UML-Klassendiagramm in Bild 2 veranschaulicht diese Beziehung. IFocusedInterface enthält nur das, was die ConsumingClass benötigt. Der Adapter ist eine sehr dünne Schicht, die IFocusedInterface implementiert und eine Abhängigkeit von IUglyInterface verwendet, um ihre Bedürfnisse zu erfüllen.
UML-Klassendiagramm, das die Abhängigkeiten illustriert (Bild 2)
Autor
Vermeiden Sie die Anwendung von DRY außerhalb der GrenzenAls angehender Entwickler wird Ihnen beigebracht, keine Duplikate in Ihrer Codebasis zuzulassen und den Grundsatz „Don’t Repeat Yourself“ (DRY) anzuwenden, um Ihren Code rigoros zu refaktorisieren. Was man Ihnen nicht gesagt hat, ist, dass DRY auch zu unnötiger Kopplung führt, was wiederum das isolierte Testen von Codeeinheiten erheblich erschwert. Ich habe auf die harte Tour gelernt, mit DRY vorsichtig zu sein. Generischer Code neigt dazu, mit der Zeit immer komplexer zu werden, sodass das Beibehalten von Kopien oft zu einfacherem Code führt (auch bekannt als Keep It Simple Stupid). Im Allgemeinen wende ich DRY innerhalb funktionaler Slices an (die internen Grenzen, über die ich immer wieder spreche) und verwende gemeinsam genutzte Dienste nur, wenn ich denke, dass es das wert ist. Bild 3 soll das verdeutlichen.

Gemeinsam verwendete Dienste nur, wenn sie einen Mehrwert bringen (Bild 3)
Autor
Teststrategien
Verschwenden Sie keine Zeit darauf, sich auf die genaue Definition eines Unit-Tests zu einigenEs gibt keine allgemeingültige Definition dafür, was ein Unit-Test genau ist, auch wenn wir uns sicher alle einig sind, dass er schnell sein muss und keine Nebenwirkungen haben darf. Aber das gilt für jede Art von automatisiertem Test, auch wenn das bei Dingen wie browserbasierten und End-to-End-Tests oft schwer zu erreichen ist. Deshalb schlage ich manchmal spielerisch vor, den Begriff „appropriate-scoped tests“ (Tests mit angemessenem Umfang) zu verwenden. Manchmal ist die Einheit eine Klasse, manchmal eine Komponente, ein Modul oder etwas Größeres. Machen Sie sich also nicht die Mühe, über diese Definition zu diskutieren. Bestimmen Sie den richtigen Umfang und schreiben Sie einen gut lesbaren, selbsterklärenden Test in angemessener Größe.Richten Sie Ihren Testumfang an den internen Grenzen Ihres Systems ausMeiner Meinung nach ist das größte Anti-Muster die Annahme, dass die kleinste Testeinheit immer eine Klasse, eine funktionale Komponente oder etwas Ähnliches ist. Verstehen Sie mich nicht falsch, es kann durchaus eine Klasse sein. Sie sollten ihren Umfang aber an den internen Grenzen Ihres Systems ausrichten. In Bild 4 ist der Testumfang beispielsweise sowohl an den funktionalen Slices als auch an den einzelnen Diensten ausgerichtet, die sie enthalten.
Die Tests an den Grenzen der Systemeinheiten ausrichten (Bild 4)
Autor
Wenn Sie eine Gruppe von Klassen haben, die zusammenarbeiten, um eine Fähigkeit bereitzustellen, und die einzelnen Klassen nicht dafür ausgelegt sind, in einem anderen Kontext wiederverwendet zu werden, sollten Sie diese Klassen als eine einzige Einheit behandeln und sie auch als solche testen. Selbst wenn sich herausstellt, dass Ihr Anwendungsbereich eine Klasse ist, können Sie diese Klasse in Richtung Design Patterns umstrukturieren und einen Teil ihrer Logik neu implementieren, indem Sie zum Beispiel ein Strategy Pattern verwenden. Das ändert nichts am Umfang Ihrer Einheit.Verwenden Sie beschreibende Tests als vorübergehendes Sicherheitsnetz Nicht jeder hat den Luxus, auf der grünen Wiese beginnen zu können. Oftmals muss man sich mit altem Code auseinandersetzen, der wenige oder, schlimmer noch, gar keine Tests enthält. In diesen Fällen ist die Einführung einer Reihe von Tests, die einfach das aktuelle Verhalten bestätigen, eine gute Möglichkeit, ein temporäres Sicherheitsnetz aufzubauen.Diese Characterization Tests, wie Michael Feathers sie nennt, können hässlich, langsam, spröde und unwartbar sein. Aber das ist in Ordnung. Sie werden sie verwenden, um mit dem Refactoring der Legacy-Code-Basis zu beginnen, Nahtstellen einzuführen und die richtigen Testeinheiten zu identifizieren. Und nach und nach werden Sie diese hässlichen Tests durch gut definierte und gut eingeteilte Tests ersetzen.Verwenden Sie Mutationstests, um die Randfälle zu finden, die Ihre Testsuite übersehen hatObwohl die Codeabdeckung (Code Coverage) ein großartiges Werkzeug ist, um die Bereiche in Ihrer Codebasis zu finden, die eben nicht richtig abgedeckt sind, hängt es stark von der Art der Abdeckung ab, welche Art von Ergebnissen Sie erhalten werden. Einige Tools unterstützen Verzweigungen und Zeilenabdeckung, andere nur eines von beiden.Aber es gibt noch ein weiteres Tool, das helfen kann, und das nennt sich Mutationstest. Im .NET-Bereich können Sie zum Beispiel Stryker.NET verwenden, um automatisch eine Reihe von Codeänderungen vorzunehmen und dann zu sehen, ob Ihre aktuelle Testsuite vollständig genug ist, um diese künstlichen Fehler zu finden. Es dauert ziemlich lange, bis die Tests abgeschlossen sind, daher handelt es sich eher um ein Tool, das man gelegentlich verwendet, um Randfälle zu finden. Aber es hat uns geholfen, einige unentdeckte Fehler in Fluent Assertions zu finden.Sie brauchen alle Ebenen der automatisierten TestsIch habe einmal einen Softwarearchitekten gehört, der sagte, dass Unit-Tests schlecht seien, weil sie das Refactoring des Codes erschweren würden. Stattdessen zog er es vor, nur HTTP-API-Tests durchzuführen. Ich verstehe zwar, woher diese Aussage kommt (unangemessener Umfang von Unit-Tests), aber Sie brauchen mehrere Testschichten, wobei der größte Teil aus Unit-Tests besteht. Sie sind einfacher zu schreiben, leichter zu verstehen und laufen schneller ab. Aber erinnern Sie sich an die Testpyramide (Bild 5)? Es ist völlig normal (und wird erwartet), dass Sie Unit-Tests, komponentenübergreifende Tests, HTTP-API-Tests und browserbasierte Tests durchführen.

Sie sollten automatisierte Tests auf allen Ebenen der Testpyramide durchführen (Bild 5)
Autor
Testen Sie Dinge, die für eine separate Wiederverwendung konzipiert wurdenWenn die internen Grenzen nicht ganz klar sind, schaue ich mir oft die Wiederverwendbarkeit einer (Gruppe von) Klasse(n) an. Wenn sie auf Wiederverwendbarkeit ausgelegt sind, und das ist etwas, das sich deutlich von der potenziellen Wiederverwendbarkeit unterscheidet, tendiere ich dazu, sie separat zu testen. In anderen Fällen würde ich diese Klassen als Teil eines größeren Bereichs testen.Das hat mir schon oft geholfen, zu verhindern, dass ich zu kleine Tests durchführe und später Probleme beim Refactoring verursache. Um diese Idee besser zu veranschaulichen, betrachten Sie das folgende Beispiel: Sie können Fluent Assertions verwenden, um einen rekursiven Vergleich zwischen einer (nicht spezifizierten) Sammlung von Ereignissen und einer anonymen Sammlung von anonymen Objekten durchzuführen.
eventMonitor.OccurredEvents.Should().BeEquivalentTo(new[]
{
new
{
EventName = "PropertyChanged",
TimestampUtc = utcNow - 1.Hours(),
Parameters = new object[] { "third", "first", 123 }
},
new
{
EventName = "NonConventionalEvent",
TimestampUtc = utcNow,
Parameters = new object[] { "first", 123, "third" }
}
}, o => o.WithStrictOrdering());
Die Hauptmethode BeEquivalentTo ist nur eine Erweiterungsmethode, die die eigentliche Arbeit aus dem in Bild 6 gezeigten Diagramm an die EquivalencyValidator-Klasse delegiert, die eine Teilmenge des Codes hinter dieser Implementierung darstellt.

Aus einer Klasse wurden durch Refaktorisierungen mehrere (Bild 6)
Autor
Vor zehn Jahren war das die einzige Klasse im Code. Aber im Lauf der Jahre habe ich die Implementierung angepasst. Da es sich bei diesen Klassen jedoch nur um Implementierungsdetails handelt, ist der Testumfang gleich geblieben.Es ist völlig in Ordnung, die Datenbank in Tests einzubeziehen In der Vergangenheit wurde uns beigebracht, die Hässlichkeit einer Datenbank hinter Abstraktionen wie Repositories, Data Access Layers und ähnlichen Konzepten zu verstecken. Das Hauptziel bestand darin, den Code, der auf einer Datenbank basierte, ohne eine echte Datenbank testen zu können. Einige dieser Entwurfsmuster sind immer noch sinnvoll. Aber wir schreiben das Jahr 2023, sodass das Ausführen einer Datenbank wie SQL Server in einem Linux-Container für Tests, bei denen der Hauptzweck darin besteht, die Interaktion mit der Datenbank zu überprüfen, absolut praktikabel ist. Behandeln Sie es einfach als Werkzeug für das richtige Problem zur richtigen Zeit. Mit TestContainers for .NET können Sie so etwas ganz einfach tun:
_sqlServerContainer = new ContainerBuilder()
.WithImage("mcr.microsoft.com/mssql/" +
"server:2019-GA-ubuntu-16.04")
.WithPortBinding(1443, assignRandomHostPort: true)
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SA_PASSWORD", Password)
.WithCleanUp(cleanUp: true)
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilOperationIsSucceeded(() =>
HealthCheck(CancellationToken.None).GetAwaiter()
.GetResult(), 10))
.Build();
await _sqlServerContainer.StartAsync();
Verwenden Sie den richtigen Stil für den richtigen Satz von TestsDie meisten Entwickler sind bereits an die Idee gewöhnt, den Code in einem Test mit Inline-Kommentaren wie arrange, act und assert zu gliedern. Diese Technik verwende ich schon seit Jahren und sie funktioniert gut für zustandsbasierte Tests. Wenn das zu testende Subjekt jedoch eher als Orchestrierungseinheit dient, ist es oft sinnvoller, einen Stil im Sinne des Behavior Driven Development zu verwenden. Bibliotheken wie ChillBDD oder Machine.Specifications bieten Werkzeuge, die die Implementierung erleichtern. Mischen Sie nur ja nicht die beiden Stile in einem Test. Ein zu testender Gegenstand wird entweder mit dem einen oder dem anderen Stil getestet.Mocking zwischen den Grenzen ist völlig in Ordnung, nicht jedoch internEs scheint einen verrückten Ort zu geben, an dem die Leute darüber streiten, ob die Verwendung von Mocking-Frameworks eine gute Sache ist oder nicht. In meiner Welt ist Mocking besonders nützlich, um die Contracts zwischen den Grenzen in Ihrem System vorzutäuschen, damit Sie eine einzelne Grenze testen können, ohne von den anderen Grenzen abhängig zu sein. Da man diese Grenzen aber normalerweise als Einheiten testet, braucht man Mocking innerhalb dieser Grenzen nur selten. Wie jedes andere Werkzeug hat auch dieses seine Zeit und seinen Platz.Verwenden Sie die Codeabdeckung als Maßstab für die Reife, nicht als Ziel oder KPINur wenige Aspekte von Unit-Tests und testgetriebener Entwicklung sind so verhasst wie die Codeabdeckung. Aber wie bei allem anderen auch sehe ich darin einen großen Wert. Eine Abdeckung von 50 Prozent sagt mir zum Beispiel etwas über die (fehlende) Reife einer Codebasis. Über 90 Prozent hinauszugehen erscheint mir jedoch als Zeitverschwendung, es sei denn, man praktiziert testgetriebene Entwicklung. Dann ist es ziemlich trivial, den 95er-Bereich zu erreichen. Die Codeabdeckung ist auch ein großartiges Werkzeug, um die riskanten Bereiche der Codebasis zu ermitteln, die eine unzureichende Abdeckung aufweisen. Aber verwenden Sie sie niemals als KPI in einem Management-Dashboard. Dies kann dazu führen, dass Entwickler nicht wartbare oder unangemessen umfangreiche Tests
erstellen.
.NET-Bereich arbeiten – Roslyn-Analyzer verwendet werden. Außerdem sollte jeder generische Code, den Sie zur Unterstützung Ihrer Tests schreiben, genauso dokumentiert werden wie jede andere Bibliothek oder jeder andere Baustein. Aber vergessen Sie nicht, was weiter oben über die Anwendung von DRY im Testcode stand.Erlauben Sie Ihren Tests, als Dokumentation verwendet zu werdenEines der ursprünglichen Versprechen der testgetriebenen Entwicklung ist die Möglichkeit, die Tests als Dokumentation der APIs, ihres Aussehens und ihres Verhaltens zu verwenden. Und ich kann Ihnen aus erster Hand sagen, dass sich dieses Versprechen erfüllen kann. Bei meinem eigenen Open-Source-Projekt Fluent Assertions bin ich überzeugt, dass wir diesen Punkt erreicht haben. Ich verwende oft einen Link zu einem bestimmten Test, um zu erklären, wie man das Beste aus dieser kleinen Bibliothek herausholen kann. Dazu müssen Sie aber Tests schreiben, die einen klaren und funktionalen Titel haben, deren Code selbsterklärend ist und die Test-Assertions verwenden, die helfen, die Ursache und Wirkung zu verstehen.Vermeiden Sie die Rückgabe von Mocks aus MocksStellen Sie sich eine DatabaseManager-Klasse vor, deren Aufgabe es ist, ein Datenbankschema zu verwalten. Wenn Sie ein Mock einer IDatabaseAdapterFactory (und ich verwende den Begriff Mock großzügig) erstellen müssen, das ein Mock eines IDatabaseAdapters zurückgibt, um den Manager zu testen, sind die Chancen groß, dass Sie Implementierungsdetails testen. Noch schlimmer ist es, wenn der zu testende Gegenstand auch Mocks anderer Abhängigkeiten benötigt. In diesem speziellen Beispiel würde ich es vorziehen, die tatsächliche Datenbank mit einem SQL Server/Linux-Container oder SqlLocalDB einzubinden und die Mocks ganz wegzulassen. In anderen Situationen kann die Anwendung des Dependency Inversion Principle und eines Adapters zur Überbrückung Ihres neuen und sauberen Vertrags mit den hässlichen Abhängigkeiten wirklich helfen.Unwichtige Dinge ausblenden, aber zeigen, was wichtig istEines der besten Dinge, die Sie tun können, um einen Test lesbarer und selbsterklärend zu machen, ist das Ausblenden des Rauschens, das nicht zu diesem speziellen Test beiträgt. Die Verwendung von Test Data Buildern oder Object Mothers ist eine großartige Möglichkeit, um die Konstruktion von Varianten eines Testobjekts wesentlich übersichtlicher zu gestalten. Achten Sie aber darauf, dass Sie sie nicht überstrapazieren. Denn: Don’t repeat yourself (DRY) ist etwas, das Sie auch in Tests sparsam einsetzen sollten.Aber auch das Gegenteil gilt: Wenn ein Aspekt der Konstruktionslogik für einen bestimmten Test wichtig ist, zeigen Sie diese Details. Ein Test, der eine HTTP-Anfrage verwendet, sollte zum Beispiel sowohl die Route als auch die Abfrageparameter anzeigen.Sicherstellen, dass Ihre Tests aus den richtigen Gründen fehlschlagen oder erfolgreich sindAls guter TDD-Praktiker ist das Erste, was Sie natürlich tun, wenn Sie versuchen, einen Fehler zu reproduzieren, einen Test zu schreiben und sicherzustellen, dass er fehlschlägt. Und nicht nur das: Er muss mit der richtigen Fehlermeldung, Ausnahme oder sogar einer spezifischen Ausnahmemeldung fehlschlagen. Aber machen Sie es nicht zu spezifisch. Es reicht aus, einen Platzhalter zu verwenden, um zu sehen, ob ein bestimmter Ausdruck in der Ausnahmemeldung erscheint. Und wenn dies nicht der Fall ist, haben Sie vielleicht das Falsche getestet. Manchmal, nachdem ich einen Fehler behoben und einen grünen Test erhalten habe, entferne ich die Korrektur, um zu sehen, ob der Test erneut fehlschlägt. Nur um auf der sicheren Seite zu sein.Bevorzugen Sie in Tests Inline-Literale und Zahlen gegenüber KonstantenAls Entwickler wurden wir rigoros darauf trainiert, magische Zahlen im Code zu vermeiden und stattdessen richtig benannte Konstanten zu definieren. Allerdings glaube ich, dass Sie dies in Ihren Tests ebenfalls vermeiden sollten. Diese magischen Zahlen machen den Test schwieriger zu lesen. Stattdessen verwende ich lieber erkennbare Zahlen wie 123 und wörtliche Zeichenfolgen wie TheCustomer oder SomeClient. Ich zeige auch gerne, wie eine bestimmte numerische Erwartung bis zu einem gewissen Grad berechnet wurde. All dies, um die Testfälle selbsterklärend zu halten.Verwenden Sie keinen Produktionscode, um das Ergebnis eines Tests zu bestätigenAngenommen, Sie haben ein HTTP-API, das mit einem plattformspezifischen Konzept implementiert ist, zum Beispiel einem ASP.NET-Controller, der einen .NET-Typ mit Eigenschaften zurückgibt, die in JSON serialisiert werden. Um dieses HTTP-API in einem Unit-Test zu testen, könnten Sie versucht sein, denselben .NET-Typ zu verwenden, der im Controller verwendet wird, um das JSON zu deserialisieren.Tun Sie das nicht. Sie wollen unbedingt, dass der Test fehlschlägt, wenn Sie die zurückgegebenen Daten ändern. Das Gleiche gilt für die Verwendung von Aufzählungswerten und anderem Produktionscode, den Sie möglicherweise in Tests verwenden möchten. Aus diesem Grund habe ich in Fluent Assertions die Option eingebaut, ein Objekt mit einem anonymen Typ zu vergleichen:
erstellen.
Testentwurf
Beobachten Sie dasselbe API oder denselben Contract, der in der Produktion verwendet wirdÜberlegen Sie beim Schreiben Ihrer Tests, welcher Teil des zu testenden Objekts das zu testende API repräsentiert. Für mich ist das offensichtlich. Es sollte die gleiche Oberfläche bieten, die auch in der Produktion verwendet wird. Wenn Sie ein HTTP-API mit .NET ASP.NET Core erstellen, senden Sie HTTP-Anfragen mit dem Test-HostBuilder und beobachten das JSON und die Header der Antwort. Rufen Sie die Methoden der Controller-Klasse nicht direkt auf. Wenn Sie eine Fassade über Ihrer Datenbank testen, verwenden Sie nur die von der Fassade bereitgestellten Methoden und vermeiden Sie den Zugriff auf die zugrunde liegende Datenbank.Schreiben Sie Ihren Testcode so, wie Sie Ihren Produktionscode schreibenDie Überschrift dieses Punktes ist eigentlich selbsterklärend: Auch Testcode ist Code und sollte die gleichen Standards erfüllen wie all Ihr anderer Code (vorausgesetzt, dieser ist in Topform). Es ist also völlig in Ordnung, wenn Ihr Testcode von automatisierten Tools wie SonarQube überprüft wird, mit einer .editorconfig-Datei formatiert wird und – wenn Sie im.NET-Bereich arbeiten – Roslyn-Analyzer verwendet werden. Außerdem sollte jeder generische Code, den Sie zur Unterstützung Ihrer Tests schreiben, genauso dokumentiert werden wie jede andere Bibliothek oder jeder andere Baustein. Aber vergessen Sie nicht, was weiter oben über die Anwendung von DRY im Testcode stand.Erlauben Sie Ihren Tests, als Dokumentation verwendet zu werdenEines der ursprünglichen Versprechen der testgetriebenen Entwicklung ist die Möglichkeit, die Tests als Dokumentation der APIs, ihres Aussehens und ihres Verhaltens zu verwenden. Und ich kann Ihnen aus erster Hand sagen, dass sich dieses Versprechen erfüllen kann. Bei meinem eigenen Open-Source-Projekt Fluent Assertions bin ich überzeugt, dass wir diesen Punkt erreicht haben. Ich verwende oft einen Link zu einem bestimmten Test, um zu erklären, wie man das Beste aus dieser kleinen Bibliothek herausholen kann. Dazu müssen Sie aber Tests schreiben, die einen klaren und funktionalen Titel haben, deren Code selbsterklärend ist und die Test-Assertions verwenden, die helfen, die Ursache und Wirkung zu verstehen.Vermeiden Sie die Rückgabe von Mocks aus MocksStellen Sie sich eine DatabaseManager-Klasse vor, deren Aufgabe es ist, ein Datenbankschema zu verwalten. Wenn Sie ein Mock einer IDatabaseAdapterFactory (und ich verwende den Begriff Mock großzügig) erstellen müssen, das ein Mock eines IDatabaseAdapters zurückgibt, um den Manager zu testen, sind die Chancen groß, dass Sie Implementierungsdetails testen. Noch schlimmer ist es, wenn der zu testende Gegenstand auch Mocks anderer Abhängigkeiten benötigt. In diesem speziellen Beispiel würde ich es vorziehen, die tatsächliche Datenbank mit einem SQL Server/Linux-Container oder SqlLocalDB einzubinden und die Mocks ganz wegzulassen. In anderen Situationen kann die Anwendung des Dependency Inversion Principle und eines Adapters zur Überbrückung Ihres neuen und sauberen Vertrags mit den hässlichen Abhängigkeiten wirklich helfen.Unwichtige Dinge ausblenden, aber zeigen, was wichtig istEines der besten Dinge, die Sie tun können, um einen Test lesbarer und selbsterklärend zu machen, ist das Ausblenden des Rauschens, das nicht zu diesem speziellen Test beiträgt. Die Verwendung von Test Data Buildern oder Object Mothers ist eine großartige Möglichkeit, um die Konstruktion von Varianten eines Testobjekts wesentlich übersichtlicher zu gestalten. Achten Sie aber darauf, dass Sie sie nicht überstrapazieren. Denn: Don’t repeat yourself (DRY) ist etwas, das Sie auch in Tests sparsam einsetzen sollten.Aber auch das Gegenteil gilt: Wenn ein Aspekt der Konstruktionslogik für einen bestimmten Test wichtig ist, zeigen Sie diese Details. Ein Test, der eine HTTP-Anfrage verwendet, sollte zum Beispiel sowohl die Route als auch die Abfrageparameter anzeigen.Sicherstellen, dass Ihre Tests aus den richtigen Gründen fehlschlagen oder erfolgreich sindAls guter TDD-Praktiker ist das Erste, was Sie natürlich tun, wenn Sie versuchen, einen Fehler zu reproduzieren, einen Test zu schreiben und sicherzustellen, dass er fehlschlägt. Und nicht nur das: Er muss mit der richtigen Fehlermeldung, Ausnahme oder sogar einer spezifischen Ausnahmemeldung fehlschlagen. Aber machen Sie es nicht zu spezifisch. Es reicht aus, einen Platzhalter zu verwenden, um zu sehen, ob ein bestimmter Ausdruck in der Ausnahmemeldung erscheint. Und wenn dies nicht der Fall ist, haben Sie vielleicht das Falsche getestet. Manchmal, nachdem ich einen Fehler behoben und einen grünen Test erhalten habe, entferne ich die Korrektur, um zu sehen, ob der Test erneut fehlschlägt. Nur um auf der sicheren Seite zu sein.Bevorzugen Sie in Tests Inline-Literale und Zahlen gegenüber KonstantenAls Entwickler wurden wir rigoros darauf trainiert, magische Zahlen im Code zu vermeiden und stattdessen richtig benannte Konstanten zu definieren. Allerdings glaube ich, dass Sie dies in Ihren Tests ebenfalls vermeiden sollten. Diese magischen Zahlen machen den Test schwieriger zu lesen. Stattdessen verwende ich lieber erkennbare Zahlen wie 123 und wörtliche Zeichenfolgen wie TheCustomer oder SomeClient. Ich zeige auch gerne, wie eine bestimmte numerische Erwartung bis zu einem gewissen Grad berechnet wurde. All dies, um die Testfälle selbsterklärend zu halten.Verwenden Sie keinen Produktionscode, um das Ergebnis eines Tests zu bestätigenAngenommen, Sie haben ein HTTP-API, das mit einem plattformspezifischen Konzept implementiert ist, zum Beispiel einem ASP.NET-Controller, der einen .NET-Typ mit Eigenschaften zurückgibt, die in JSON serialisiert werden. Um dieses HTTP-API in einem Unit-Test zu testen, könnten Sie versucht sein, denselben .NET-Typ zu verwenden, der im Controller verwendet wird, um das JSON zu deserialisieren.Tun Sie das nicht. Sie wollen unbedingt, dass der Test fehlschlägt, wenn Sie die zurückgegebenen Daten ändern. Das Gleiche gilt für die Verwendung von Aufzählungswerten und anderem Produktionscode, den Sie möglicherweise in Tests verwenden möchten. Aus diesem Grund habe ich in Fluent Assertions die Option eingebaut, ein Objekt mit einem anonymen Typ zu vergleichen:
var host = host.GetTestClient();
HttpResponseMessage response = await host.GetAsync(
$"http://localhost/statistics/metrics/" +
"CountsPerState?country={countryCode}&kind=Filming");
string body =
await response.Content.ReadAsStringAsync();
var expectation = new[]
{
new
{
State = "Active",
Count = 1
}
};
var actual = JsonConvert.DeserializeAnonymousType(
body, expectation);
actual.Should().BeEquivalentTo(expectation);
Nehmen Sie nur Annahmen auf, die für den jeweiligen Testfall relevant sindIn engem Zusammenhang mit der Verwendung von Produktionscode ist sicherzustellen, dass Ihre Behauptungen nur den Teil abdecken, den Sie benötigen, um zu beweisen, dass sich der zu testende Gegenstand wie erwartet verhält. Wenn es zum Beispiel nicht ausreicht, zu behaupten, dass eine bestimmte Art von Ausnahme ausgelöst wurde, sollten Sie auch einen Teil der Nachricht in die Behauptung aufnehmen.Achten Sie nur darauf, dass Sie nur die Teile aufnehmen, die Sie benötigen. Aus diesem Grund kann WithMessage von Fluent Assertion auch Platzhalter enthalten. Jeder Testfall sollte einen bestimmten Zweck haben, also sorgen Sie dafür, dass der Test nur dann fehlschlägt, wenn dieser bestimmte Zweck nicht erfüllt wurde.Schreiben Sie Assertions, die Sie aus der Debugger-Hölle heraushaltenMeiner Meinung nach hat ein großartiger Test einen Titel, der das erwartete Verhalten erklärt, Code, der die Ursache und Wirkung deutlich zeigt, und eine selbsterklärende Fehlermeldung, falls der Test fehlschlägt. Mit diesen drei Elementen ist die Wahrscheinlichkeit, dass Sie Ihren Debugger einschalten müssen, um zu verstehen, was passiert, gering. Ein großartiger Titel hilft Ihnen, den Zweck des Tests zu verstehen, eine großartige Implementierung hilft Ihnen zu verstehen, wie dieser Zweck umgesetzt wird, und eine großartige Fehlermeldung hilft Ihnen zu verstehen, was erwartet wird und was Sie mit dem richtigen Kontext bekommen haben. Dies ist eine der wichtigsten Entwurfsphilosophien hinter Fluent Assertions und die Art und Weise, wie ich Testcode betrachte.
Benennung
Postfix-Testklassen und -Dateien mit SpecsDas ultimative Versprechen von testgetriebener Entwicklung ist, dass Sie Ihre Tests als Dokumentation verwenden können. Das ist sicherlich richtig, aber es kommt nicht von allein. Wie ich bereits oben erwähnt habe, sind ein guter Titel, gut geschriebener Code und klare Fehlermeldungen entscheidende Zutaten. Etwas, was ich gerne mache, ist, alle meine Testklassen und -dateien mit Specs zu versehen. Das hat keinen technischen Einfluss, sondern soll nur ein weiterer Anstoß für die Entwickler sein, die Testfälle als Spezifikationen des Systems zu betrachten.Verwenden Sie kurze, faktenbasierte Namen und gruppieren Sie die Tests nach API oder ZweckDie Konvention [UnitOfWork]_[StateUnderTest]_[ExpectedBehavior] hat mir nie wirklich gefallen. Das Ergebnis sind Testnamen wie Sum_NegativeNumberAs1stParam_ExceptionThrown. Obwohl ich die Verwendung von Unterstrichen sehr schätze, sind diese Namen für mich viel zu technisch und kryptisch. Für mich ist der Name des Tests eine wichtige Information. Er sollte das funktionale Szenario erklären, das der Test durchzusetzen versucht, und nicht eine Beschreibung sein, wie der Test funktioniert.Lange Zeit habe ich das funktionalere When_[scenario]_it_should_[expected_behavior] bevorzugt, wie zum Beispiel When_the_same_objects_are_expected_to_be_the_same_it_should_not_fail. Das ist zwar funktional korrekt, aber ich empfinde diesen Stil immer mehr als störend und langatmig.Ein gängiges Muster, das ich in letzter Zeit angewandt habe, ist die Gruppierung von Tests mithilfe einer verschachtelten Klasse. Damit stelle ich im Wesentlichen mehr Kontext über eine Gruppe zusammengehöriger Tests bereit. Auf diese Weise kann ich sofort einige überflüssige Informationen aus dem Testnamen entfernen. Hier ist ein Beispiel dafür, wie dies für Tests aussehen könnte, die sicherstellen, dass sich das API BeSameAs wie erwartet verhält.
public class ReferenceTypeAssertionsSpecs
{
public class BeSameAs
{
[Fact]
public void
References_to_the_same_object_are_valid()
{}
[Fact]
public void
References_to_different_objects_are_invalid()
{}
}
}
Refaktorisieren Sie Ihre Tests nicht, bis Sie das Muster erkennenWeiter oben in diesem Artikel habe ich darüber geschrieben, dass man die Verwendung von DRY an den Systemgrenzen ausrichten sollte und dass man dieses Prinzip nicht global anwenden sollte, es sei denn, eine bestimmte Fähigkeit ist die Kopplung wert. Innerhalb des Testcodes müssen Sie sogar noch konservativer vorgehen. Der Versuch, Testcode zu deduplizieren, führt oft dazu, dass Details, die für die einzelnen Testfälle wichtig sein könnten, verborgen bleiben. Die Verwendung von Test Data Buildern oder Object Mothers ist immer noch eine gute Lösung, solange sie die Absicht des Testfalls nicht verschleiern. Aber versuchen Sie nicht zu refaktorisieren, bevor Sie nicht das wirkliche Muster gesehen haben. Dinge zweimal zu tun ist höchstwahrscheinlich Zufall. Dinge dreimal zu tun könnte ein Muster sein, das es wert ist, refaktorisiert zu werden.