8 Best Practices für automatisiertes Testen für ein positives Testerlebnis
Veröffentlicht: 2022-03-11Kein Wunder, dass viele Entwickler das Testen als notwendiges Übel betrachten, das Zeit und Energie frisst: Testen kann mühsam, unproduktiv und viel zu kompliziert sein.
Meine erste Erfahrung mit dem Testen war schrecklich. Ich habe in einem Team gearbeitet, das strenge Anforderungen an die Codeabdeckung hatte. Der Workflow war: Implementieren Sie ein Feature, debuggen Sie es und schreiben Sie Tests, um eine vollständige Codeabdeckung sicherzustellen. Das Team hatte keine Integrationstests, nur Unit-Tests mit Tonnen von manuell initialisierten Mocks, und die meisten Unit-Tests testeten triviale manuelle Zuordnungen, während sie eine Bibliothek zur Durchführung automatischer Zuordnungen verwendeten. Jeder Test versuchte, jede verfügbare Eigenschaft zu bestätigen, sodass jede Änderung Dutzende von Tests brach.
Ich habe nicht gerne mit Tests gearbeitet, weil sie als zeitraubende Belastung empfunden wurden. Ich habe jedoch nicht aufgegeben. Die Vertrauenstests und die Automatisierung der Überprüfung nach jeder kleinen Änderung haben mein Interesse geweckt. Ich fing an zu lesen und zu üben und lernte, dass Tests, wenn sie richtig gemacht werden, sowohl hilfreich als auch unterhaltsam sein können.
In diesem Artikel teile ich acht Best Practices für automatisiertes Testen, die ich gerne von Anfang an gewusst hätte.
Warum Sie eine automatisierte Teststrategie brauchen
Automatisiertes Testen ist oft zukunftsorientiert, aber wenn Sie es richtig implementieren, profitieren Sie sofort. Die Verwendung von Tools, die Ihnen helfen, Ihre Arbeit besser zu erledigen, kann Zeit sparen und Ihre Arbeit angenehmer machen.
Stellen Sie sich vor, Sie entwickeln ein System, das Bestellungen aus dem ERP des Unternehmens abruft und diese Bestellungen bei einem Lieferanten aufgibt. Sie haben den Preis von zuvor bestellten Artikeln im ERP, aber die aktuellen Preise können abweichen. Sie möchten steuern, ob Sie eine Bestellung zu einem niedrigeren oder höheren Preis aufgeben. Sie haben Benutzereinstellungen gespeichert und schreiben Code, um mit Preisschwankungen umzugehen.
Wie würden Sie überprüfen, ob der Code wie erwartet funktioniert? Sie würden wahrscheinlich:
- Erstellen Sie einen Dummy-Auftrag in der Entwicklerinstanz des ERP (vorausgesetzt, Sie haben ihn vorher eingerichtet).
- Führen Sie Ihre App aus.
- Wählen Sie diese Bestellung aus und starten Sie den Bestellvorgang.
- Sammeln Sie Daten aus der ERP-Datenbank.
- Fordern Sie die aktuellen Preise über die API des Anbieters an.
- Überschreiben Sie Preise im Code, um bestimmte Bedingungen zu erstellen.
Sie haben am Haltepunkt angehalten und können Schritt für Schritt sehen, was für ein Szenario passieren wird, aber es gibt viele mögliche Szenarien:
| Einstellungen | ERP-Preis | Anbieterpreis | Sollen wir die Bestellung aufgeben? | |
|---|---|---|---|---|
| Höheren Preis zulassen | Niedrigeren Preis zulassen | |||
| falsch | falsch | 10 | 10 | wahr |
| (Hier gäbe es drei weitere Präferenzkombinationen, aber die Preise sind gleich, daher ist das Ergebnis dasselbe.) | ||||
| wahr | falsch | 10 | 11 | wahr |
| wahr | falsch | 10 | 9 | falsch |
| falsch | wahr | 10 | 11 | falsch |
| falsch | wahr | 10 | 9 | wahr |
| wahr | wahr | 10 | 11 | wahr |
| wahr | wahr | 10 | 9 | wahr |
Im Falle eines Fehlers kann das Unternehmen Geld verlieren, seinen Ruf schädigen oder beides. Sie müssen mehrere Szenarien prüfen und die Prüfschleife mehrmals wiederholen. Manuell wäre das mühsam. Aber Tests sind hier, um zu helfen!
Mit Tests können Sie jeden Kontext ohne Aufrufe instabiler APIs erstellen. Sie machen das wiederholte Klicken durch alte und langsame Schnittstellen überflüssig, die in älteren ERP-Systemen allzu häufig vorkommen. Alles, was Sie tun müssen, ist, den Kontext für die Einheit oder das Subsystem zu definieren, und dann werden alle Debugging-, Fehlerbehebungs- oder Szenario-Untersuchungen sofort ausgeführt – Sie führen den Test aus und kehren zu Ihrem Code zurück. Ich ziehe es vor, eine Tastenkombination in meiner IDE einzurichten, die meinen vorherigen Testlauf wiederholt und sofortiges, automatisiertes Feedback gibt, wenn ich Änderungen vornehme.
1. Behalten Sie die richtige Einstellung bei
Im Vergleich zum manuellen Debuggen und Selbsttesten sind automatisierte Tests von Anfang an produktiver, noch bevor Testcode festgeschrieben wird. Nachdem Sie überprüft haben, ob sich Ihr Code wie erwartet verhält – durch manuelles Testen oder vielleicht, für ein komplexeres Modul, durch schrittweises Durchlaufen mit einem Debugger während des Tests –, können Sie Zusicherungen verwenden, um zu definieren, was Sie für eine beliebige Kombination von Eingabeparametern erwarten.
Wenn die Tests bestanden sind, sind Sie fast bereit, sich zu verpflichten, aber noch nicht ganz. Bereiten Sie sich darauf vor, Ihren Code umzugestalten, da die erste funktionierende Version normalerweise nicht elegant ist. Würden Sie dieses Refactoring ohne Tests durchführen? Das ist fraglich, weil Sie alle manuellen Schritte erneut durchführen müssten, was Ihre Begeisterung schmälern könnte.
Was ist mit der Zukunft? Bei der Durchführung von Refactoring, Optimierung oder Hinzufügen von Funktionen helfen Tests sicherzustellen, dass sich ein Modul nach einer Änderung immer noch wie erwartet verhält, wodurch dauerhaftes Vertrauen geschaffen wird und Entwickler sich besser gerüstet fühlen, um anstehende Aufgaben anzugehen.
Es ist kontraproduktiv, Tests als Last oder etwas zu betrachten, das nur Code-Reviewer oder Leads glücklich macht. Tests sind ein Werkzeug, von dem wir als Entwickler profitieren. Wir mögen es, wenn unser Code funktioniert, und wir verbringen keine Zeit mit sich wiederholenden Aktionen oder mit dem Korrigieren von Code, um Fehler zu beheben.
Kürzlich habe ich am Refactoring in meiner Codebasis gearbeitet und meine IDE gebeten, nicht using -Direktiven zu bereinigen. Zu meiner Überraschung zeigten Tests mehrere Fehler in meinem E-Mail-Berichtssystem. Es war jedoch ein gültiger Fehler – der Bereinigungsprozess entfernte einige using -Direktiven in meinem Razor-Code (HTML + C#) für eine E-Mail-Vorlage, und die Vorlagen-Engine konnte daher kein gültiges HTML erstellen. Ich hatte nicht erwartet, dass eine so kleine Operation die E-Mail-Berichte unterbrechen würde. Durch das Testen konnte ich vermeiden, Stunden damit zu verbringen, Fehler in der gesamten App kurz vor der Veröffentlichung zu finden, als ich davon ausging, dass alles funktionieren würde.
Natürlich muss man mit Werkzeugen umgehen können und darf sich nicht die sprichwörtlichen Finger schneiden. Es mag den Anschein haben, dass das Definieren des Kontexts mühsam und schwieriger sein kann als das Ausführen der App, dass Tests zu viel Wartung erfordern, um zu vermeiden, dass sie veraltet und nutzlos werden. Dies sind gültige Punkte und wir werden sie ansprechen.
2. Wählen Sie den richtigen Testtyp aus
Entwickler lehnen automatisierte Tests oft ab, weil sie versuchen, ein Dutzend Abhängigkeiten zu simulieren, nur um zu prüfen, ob sie vom Code aufgerufen werden. Alternativ treffen Entwickler auf einen High-Level-Test und versuchen, jeden Anwendungszustand zu reproduzieren, um alle Variationen in einem kleinen Modul zu überprüfen. Diese Muster sind unproduktiv und mühsam, aber wir können sie vermeiden, indem wir verschiedene Testtypen so einsetzen, wie sie beabsichtigt waren. (Tests sollten schließlich praktisch sein und Spaß machen!)
Die Leser müssen wissen, was Unit-Tests sind und wie man sie schreibt, und mit Integrationstests vertraut sein – wenn nicht, lohnt es sich, hier eine Pause einzulegen, um sich auf den neuesten Stand zu bringen.
Es gibt Dutzende von Testtypen, aber diese fünf gängigen Typen ergeben eine äußerst effektive Kombination:
- Unit-Tests werden verwendet, um ein isoliertes Modul zu testen, indem seine Methoden direkt aufgerufen werden. Abhängigkeiten werden nicht getestet, daher werden sie verspottet.
- Integrationstests werden verwendet, um Subsysteme zu testen. Sie verwenden immer noch direkte Aufrufe der eigenen Methoden des Moduls, aber hier kümmern wir uns um Abhängigkeiten, verwenden Sie also keine verspotteten Abhängigkeiten – nur echte (produktions-) abhängige Module. Sie können weiterhin eine In-Memory-Datenbank oder einen simulierten Webserver verwenden, da es sich hierbei um Nachahmungen der Infrastruktur handelt.
- Funktionstests sind Tests für die gesamte Anwendung, die auch als End-to-End (E2E)-Tests bezeichnet werden. Du verwendest keine Direktrufe. Stattdessen läuft die gesamte Interaktion über die API oder Benutzeroberfläche – das sind die Tests aus der Sicht des Endbenutzers. Allerdings wird die Infrastruktur immer noch verspottet.
- Canary-Tests ähneln funktionalen Tests, verfügen jedoch über eine Produktionsinfrastruktur und einen kleineren Satz von Aktionen. Sie werden verwendet, um sicherzustellen, dass neu bereitgestellte Anwendungen funktionieren.
- Lasttests ähneln Canary-Tests, verfügen jedoch über eine echte Staging-Infrastruktur und einen noch kleineren Satz von Aktionen, die viele Male wiederholt werden.
Es ist nicht immer notwendig, von Anfang an mit allen fünf Testarten zu arbeiten. In den meisten Fällen können Sie mit den ersten drei Tests viel erreichen.
Wir werden die Anwendungsfälle jedes Typs kurz untersuchen, um Ihnen bei der Auswahl der richtigen für Ihre Anforderungen zu helfen.
Unit-Tests
Erinnern Sie sich an das Beispiel mit unterschiedlichen Preisen und Handhabungspräferenzen. Es ist ein guter Kandidat für Komponententests, da wir uns nur darum kümmern, was innerhalb des Moduls passiert, und die Ergebnisse wichtige geschäftliche Auswirkungen haben.
Das Modul hat viele verschiedene Kombinationen von Eingabeparametern, und wir möchten einen gültigen Rückgabewert für jede Kombination gültiger Argumente erhalten. Komponententests sind gut geeignet, um die Gültigkeit sicherzustellen, da sie direkten Zugriff auf die Eingabeparameter der Funktion oder Methode bieten und Sie nicht Dutzende von Testmethoden schreiben müssen, um jede Kombination abzudecken. In vielen Sprachen können Sie das Duplizieren von Testmethoden vermeiden, indem Sie eine Methode definieren, die Argumente akzeptiert, die für Ihren Code und die erwarteten Ergebnisse erforderlich sind. Anschließend können Sie Ihre Testwerkzeuge verwenden, um verschiedene Sätze von Werten und Erwartungen für diese parametrisierte Methode bereitzustellen.
Integrationstests
Integrationstests eignen sich gut für Fälle, in denen Sie daran interessiert sind, wie ein Modul mit seinen Abhängigkeiten, anderen Modulen oder der Infrastruktur interagiert. Sie verwenden immer noch direkte Methodenaufrufe, aber es gibt keinen Zugriff auf Submodule, daher ist der Versuch, alle Szenarien für alle Eingabemethoden aller Submodule zu testen, unpraktisch.
Normalerweise bevorzuge ich ein Erfolgsszenario und ein Fehlerszenario pro Modul.
Ich verwende gerne Integrationstests, um zu überprüfen, ob ein Abhängigkeitsinjektionscontainer erfolgreich erstellt wurde, ob eine Verarbeitungs- oder Berechnungspipeline das erwartete Ergebnis zurückgibt oder ob komplexe Daten korrekt aus einer Datenbank oder einer Drittanbieter-API gelesen und konvertiert wurden.
Funktions- oder E2E-Tests
Diese Tests geben Ihnen die größte Sicherheit, dass Ihre App funktioniert, da sie überprüfen, ob Ihre App zumindest ohne Laufzeitfehler gestartet werden kann. Es ist etwas mehr Arbeit, mit dem Testen Ihres Codes ohne direkten Zugriff auf seine Klassen zu beginnen, aber sobald Sie die ersten paar Tests verstanden und geschrieben haben, werden Sie feststellen, dass es nicht allzu schwierig ist.
Führen Sie die Anwendung aus, indem Sie bei Bedarf einen Prozess mit Befehlszeilenargumenten starten, und verwenden Sie die Anwendung dann so, wie es Ihr potenzieller Kunde tun würde: durch Aufrufen von API-Endpunkten oder Drücken von Schaltflächen. Das ist selbst beim UI-Testen nicht schwierig: Jede große Plattform hat ein Tool, um ein visuelles Element in einer UI zu finden.
Kanarienvogel-Tests
Funktionstests lassen Sie wissen, ob Ihre App in einer Testumgebung funktioniert, aber was ist mit einer Produktionsumgebung? Angenommen, Sie arbeiten mit mehreren APIs von Drittanbietern und möchten ein Dashboard mit deren Status haben oder sehen, wie Ihre Anwendung eingehende Anfragen verarbeitet. Dies sind häufige Anwendungsfälle für Canary-Tests.
Sie wirken, indem sie kurzzeitig auf das funktionierende System einwirken, ohne Seiteneffekte auf Drittsysteme zu verursachen. Sie können beispielsweise einen neuen Benutzer registrieren oder die Produktverfügbarkeit prüfen, ohne eine Bestellung aufzugeben.
Der Zweck von Canary-Tests besteht darin, sicherzustellen, dass alle wichtigen Komponenten in einer Produktionsumgebung zusammenarbeiten und nicht beispielsweise aufgrund von Anmeldeinformationen scheitern.
Belastungstests
Belastungstests zeigen, ob Ihre Anwendung auch dann noch funktioniert, wenn viele Menschen sie verwenden. Sie ähneln Canary- und Funktionstests, werden jedoch nicht in lokalen oder Produktionsumgebungen durchgeführt. Üblicherweise wird eine spezielle Staging-Umgebung verwendet, die der Produktionsumgebung ähnelt.
Es ist wichtig zu beachten, dass diese Tests keine echten Dienste von Drittanbietern verwenden, die möglicherweise mit externen Lasttests ihrer Produktionsdienste unzufrieden sind und daher möglicherweise zusätzliche Gebühren erheben.
3. Halten Sie die Testtypen getrennt
Bei der Erstellung Ihres automatisierten Testplans sollte jeder Testtyp getrennt werden, damit er unabhängig ausgeführt werden kann. Dies erfordert zwar zusätzliche Organisation, lohnt sich aber, da Mischtests Probleme bereiten können.
Diese Tests haben unterschiedliche:
- Absichten und Grundkonzepte (so dass ihre Trennung einen guten Präzedenzfall für die nächste Person darstellt, die sich den Code ansieht, einschließlich „Future You“).
- Ausführungszeiten (das erste Ausführen von Komponententests ermöglicht also einen schnelleren Testzyklus, wenn ein Test fehlschlägt).
- Abhängigkeiten (daher ist es effizienter, nur die innerhalb eines Testtyps benötigten zu laden).
- Erforderliche Infrastrukturen.
- Programmiersprachen (in bestimmten Fällen).
- Positionen in der Continuous Integration (CI)-Pipeline oder außerhalb.
Es ist wichtig zu beachten, dass Sie bei den meisten Sprachen und Tech-Stacks beispielsweise alle Komponententests zusammen mit Unterordnern gruppieren können, die nach Funktionsmodulen benannt sind. Dies ist bequem, reduziert die Reibung beim Erstellen neuer Funktionsmodule, erleichtert automatisierte Builds, führt zu weniger Unordnung und ist eine weitere Möglichkeit, das Testen zu vereinfachen.
4. Führen Sie Ihre Tests automatisch durch
Stellen Sie sich eine Situation vor, in der Sie einige Tests geschrieben haben, aber nachdem Sie Ihr Repo ein paar Wochen später abgerufen haben, stellen Sie fest, dass diese Tests nicht mehr bestehen.
Dies ist eine unangenehme Erinnerung daran, dass Tests Code sind und wie jeder andere Teil des Codes gewartet werden müssen. Der beste Zeitpunkt dafür ist kurz vor dem Moment, in dem Sie denken, dass Sie mit Ihrer Arbeit fertig sind und sehen möchten, ob noch alles so funktioniert, wie Sie es sich vorgestellt haben. Sie verfügen über den gesamten erforderlichen Kontext und können den Code leichter reparieren oder fehlgeschlagene Tests ändern als Ihr Kollege, der an einem anderen Subsystem arbeitet. Aber dieser Moment existiert nur in Ihrem Kopf, daher ist die häufigste Art, Tests auszuführen, automatisch nach einem Push an den Entwicklungszweig oder nach dem Erstellen einer Pull-Anfrage.

Auf diese Weise befindet sich Ihre Hauptniederlassung immer in einem gültigen Zustand, oder Sie haben zumindest eine klare Anzeige ihres Zustands. Eine automatisierte Erstellungs- und Testpipeline – oder eine CI-Pipeline – hilft:
- Stellen Sie sicher, dass der Code erstellbar ist.
- Beseitigen Sie potenzielle „Es funktioniert auf meinem Computer“ -Probleme.
- Stellen Sie ausführbare Anweisungen zur Vorbereitung einer Entwicklungsumgebung bereit.
Das Konfigurieren dieser Pipeline nimmt Zeit in Anspruch, aber die Pipeline kann eine Reihe von Problemen aufdecken, bevor sie Benutzer oder Clients erreichen, selbst wenn Sie der einzige Entwickler sind.
Einmal ausgeführt, deckt CI auch neue Probleme auf, bevor sie die Chance haben, an Umfang zuzunehmen. Daher ziehe ich es vor, es direkt nach dem Schreiben des ersten Tests einzurichten. Sie können Ihren Code in einem privaten Repository auf GitHub hosten und GitHub-Aktionen einrichten. Wenn Ihr Repo öffentlich ist, haben Sie noch mehr Optionen als GitHub-Aktionen. Mein automatisierter Testplan läuft beispielsweise auf AppVeyor für ein Projekt mit einer Datenbank und drei Arten von Tests.
Ich strukturiere meine Pipeline für Produktionsprojekte am liebsten wie folgt:
- Zusammenstellung oder Transpilation
- Komponententests: Sie sind schnell und erfordern keine Abhängigkeiten
- Einrichtung und Initialisierung der Datenbank oder anderer Dienste
- Integrationstests: Sie haben Abhängigkeiten außerhalb Ihres Codes, sind aber schneller als Funktionstests
- Funktionstests: Wenn andere Schritte erfolgreich abgeschlossen wurden, führen Sie die gesamte App aus
Es gibt keine Canary-Tests oder Belastungstests. Aufgrund ihrer Besonderheiten und Anforderungen sollten sie manuell initiiert werden.
5. Schreiben Sie nur notwendige Tests
Das Schreiben von Komponententests für den gesamten Code ist eine gängige Strategie, aber manchmal verschwendet dies Zeit und Energie und gibt Ihnen kein Vertrauen. Wenn Sie mit dem Konzept der „Testpyramide“ vertraut sind, denken Sie vielleicht, dass Ihr gesamter Code mit Unit-Tests abgedeckt werden muss und nur eine Teilmenge von anderen Tests auf höherer Ebene abgedeckt wird.
Ich sehe keine Notwendigkeit, einen Komponententest zu schreiben, der sicherstellt, dass mehrere verspottete Abhängigkeiten in der gewünschten Reihenfolge aufgerufen werden. Dazu müssen mehrere Mocks eingerichtet und alle Aufrufe überprüft werden, aber es würde mir immer noch nicht die Gewissheit geben, dass das Modul funktioniert. Normalerweise schreibe ich nur einen Integrationstest, der echte Abhängigkeiten verwendet und nur das Ergebnis überprüft; Das gibt mir ein gewisses Vertrauen, dass die Pipeline im getesteten Modul ordnungsgemäß funktioniert.
Im Allgemeinen schreibe ich Tests, die mir das Leben erleichtern, während ich die Funktionalität implementiere und später betreue.
Für die meisten Anwendungen bedeutet das Ziel einer 100-prozentigen Codeabdeckung eine Menge mühsamer Arbeit und macht die Arbeit mit Tests und Programmierung im Allgemeinen zunichte. Wie Martin Fowlers Test Coverage es ausdrückt:
Die Testabdeckung ist ein nützliches Werkzeug, um ungetestete Teile einer Codebasis zu finden. Die Testabdeckung ist als numerische Aussage darüber, wie gut Ihre Tests sind, wenig aussagekräftig.
Daher empfehle ich Ihnen, den Coverage Analyzer zu installieren und auszuführen, nachdem Sie einige Tests geschrieben haben. Der Bericht mit hervorgehobenen Codezeilen hilft Ihnen, die Ausführungspfade besser zu verstehen und nicht abgedeckte Stellen zu finden, die abgedeckt werden sollten. Wenn Sie sich Ihre Getter, Setter und Fassaden ansehen, werden Sie auch sehen, warum eine 100%ige Abdeckung keinen Spaß macht.
6. Lego spielen
Von Zeit zu Zeit sehe ich Fragen wie: „Wie kann ich private Methoden testen?“ Du nicht. Wenn Sie diese Frage gestellt haben, ist bereits etwas schief gelaufen. Normalerweise bedeutet dies, dass Sie gegen das Prinzip der Einzelverantwortung verstoßen haben und Ihr Modul etwas nicht richtig macht.
Gestalten Sie dieses Modul um und ziehen Sie die Logik, die Sie für wichtig halten, in ein separates Modul. Es ist kein Problem, die Anzahl der Dateien zu erhöhen, was zu einem Code führt, der wie Legosteine strukturiert ist: sehr gut lesbar, wartbar, austauschbar und testbar.
Code richtig zu strukturieren ist leichter gesagt als getan. Hier sind zwei Vorschläge:
Funktionale Programmierung
Es lohnt sich, etwas über die Prinzipien und Ideen der funktionalen Programmierung zu lernen. Die meisten Mainstream-Sprachen wie C, C++, C#, Java, Assembly, JavaScript und Python zwingen Sie dazu, Programme für Maschinen zu schreiben. Die funktionale Programmierung ist besser für das menschliche Gehirn geeignet.
Dies mag auf den ersten Blick kontraintuitiv erscheinen, aber bedenken Sie Folgendes: Ein Computer ist in Ordnung, wenn Sie Ihren gesamten Code in einer einzigen Methode ablegen, einen gemeinsam genutzten Speicherblock zum Speichern temporärer Werte verwenden und eine angemessene Menge Sprunganweisungen verwenden. Darüber hinaus tun Compiler in der Optimierungsphase dies manchmal. Das menschliche Gehirn bewältigt diesen Ansatz jedoch nicht so einfach.
Die funktionale Programmierung zwingt Sie dazu, reine Funktionen ohne Seiteneffekte mit starken Typen auf ausdrucksstarke Weise zu schreiben. Auf diese Weise ist es viel einfacher, über eine Funktion nachzudenken, da das einzige, was sie produziert, ihr Rückgabewert ist. Die Podcast-Folge „Programming Throwdown“ „Funktionale Programmierung mit Adam Gordon Bell“ hilft Ihnen dabei, ein grundlegendes Verständnis zu erlangen, und Sie können mit den Corecursive-Folgen „Gottes Programmiersprache mit Philip Wadler“ und „Kategorientheorie mit Bartosz Milewski“ fortfahren. Die letzten beiden haben meine Wahrnehmung des Programmierens sehr bereichert.
Testgetriebene Entwicklung
Ich empfehle, TDD zu beherrschen. Der beste Weg zu lernen ist zu üben. String Calculator Kata ist eine großartige Möglichkeit, mit Code-Kata zu üben. Das Beherrschen der Kata wird einige Zeit in Anspruch nehmen, aber letztendlich wird es Ihnen ermöglichen, die Idee von TDD vollständig aufzunehmen, was Ihnen dabei helfen wird, gut strukturierten Code zu erstellen, mit dem Sie gerne arbeiten und der auch testbar ist.
Ein Hinweis zur Vorsicht: Manchmal werden Sie TDD-Puristen sehen, die behaupten, TDD sei der einzig richtige Weg zum Programmieren. Meiner Meinung nach ist es einfach ein weiteres nützliches Werkzeug in Ihrem Werkzeugkasten, mehr nicht.
Manchmal müssen Sie sehen, wie Sie Module und Prozesse aufeinander abstimmen und wissen nicht, welche Daten und Signaturen Sie verwenden sollen. Schreiben Sie in solchen Fällen Code, bis er kompiliert wird, und schreiben Sie dann Tests, um Fehler zu beheben und die Funktionalität zu debuggen.
In anderen Fällen kennen Sie die gewünschte Eingabe und Ausgabe, haben aber aufgrund komplizierter Logik keine Ahnung, wie Sie die Implementierung richtig schreiben sollen. In diesen Fällen ist es einfacher, dem TDD-Verfahren zu folgen und Ihren Code Schritt für Schritt zu erstellen, anstatt Zeit damit zu verbringen, über die perfekte Implementierung nachzudenken.
7. Halten Sie Tests einfach und fokussiert
Es macht Spaß, in einer ordentlich organisierten Codeumgebung ohne unnötige Ablenkungen zu arbeiten. Aus diesem Grund ist es wichtig, die SOLID-, KISS- und DRY-Prinzipien auf Tests anzuwenden und bei Bedarf Refactoring einzusetzen.
Manchmal höre ich Kommentare wie: „Ich hasse es, in einer stark getesteten Codebasis zu arbeiten, weil jede Änderung erfordert, dass ich Dutzende von Tests behebe.“ Das ist ein wartungsintensives Problem, das durch Tests verursacht wird, die nicht fokussiert sind und versuchen, zu viel zu testen. Auch bei Tests gilt das Prinzip „Do one thing well“: „Test one thing well“; Jeder Test sollte relativ kurz sein und nur ein Konzept testen. „Eine Sache gut testen“ bedeutet nicht, dass Sie sich auf eine Aussage pro Test beschränken sollten: Sie können Dutzende verwenden, wenn Sie nicht triviale und wichtige Datenzuordnungen testen.
Dieser Fokus ist nicht auf einen bestimmten Test oder Testtyp beschränkt. Stellen Sie sich vor, Sie hätten es mit komplizierter Logik zu tun, die Sie mithilfe von Unit-Tests getestet haben, z. B. das Zuordnen von Daten aus dem ERP-System zu Ihrer Struktur, und Sie haben einen Integrationstest, der auf Schein-ERP-APIs zugreift und das Ergebnis zurückgibt. In diesem Fall ist es wichtig, sich daran zu erinnern, was Ihr Komponententest bereits abdeckt, damit Sie die Zuordnung nicht erneut in Integrationstests testen. Normalerweise reicht es aus, sicherzustellen, dass das Ergebnis das richtige Identifikationsfeld enthält.
Mit Code, der wie Legosteine strukturiert ist, und fokussierten Tests sollten Änderungen an der Geschäftslogik nicht schmerzhaft sein. Bei radikalen Änderungen löschen Sie einfach die Datei und die zugehörigen Tests und erstellen eine neue Implementierung mit neuen Tests. Bei geringfügigen Änderungen ändern Sie in der Regel ein bis drei Tests, um die neuen Anforderungen zu erfüllen, und nehmen Änderungen an der Logik vor. Es ist in Ordnung, Tests zu ändern; Sie können sich diese Praxis als doppelte Buchführung vorstellen.
Andere Möglichkeiten, um Einfachheit zu erreichen, sind:
- Konventionen für die Strukturierung von Testdateien, die Strukturierung von Testinhalten (typischerweise eine Arrange-Act-Assert-Struktur) und die Benennung von Tests entwickeln; dann, was am wichtigsten ist, befolgen Sie diese Regeln konsequent.
- Extrahieren Sie große Codeblöcke in Methoden wie „Anfrage vorbereiten“ und erstellen Sie Hilfsfunktionen für wiederholte Aktionen.
- Anwenden des Builder-Musters für die Testdatenkonfiguration.
- Verwendung (in Integrationstests) desselben DI-Containers, den Sie in der Haupt-App verwenden, sodass jede Instanziierung so trivial ist wie
TestServices.Get(), ohne dass Abhängigkeiten manuell erstellt werden müssen. Auf diese Weise wird es einfach sein, neue Tests zu lesen, zu warten und zu schreiben, da Sie bereits nützliche Helfer haben.
Wenn Sie das Gefühl haben, dass ein Test zu kompliziert wird, halten Sie einfach inne und denken Sie nach. Entweder das Modul oder Ihr Test muss umgestaltet werden.
8. Verwenden Sie Tools, um Ihr Leben einfacher zu machen
Beim Testen werden Sie mit vielen mühsamen Aufgaben konfrontiert. Beispielsweise das Einrichten von Testumgebungen oder Datenobjekten, das Konfigurieren von Stubs und Mocks für Abhängigkeiten und so weiter. Glücklicherweise enthält jeder ausgereifte Tech-Stack mehrere Tools, um diese Aufgaben viel weniger mühsam zu machen.
Ich schlage vor, dass Sie Ihre ersten hundert Tests schreiben, falls Sie dies noch nicht getan haben, und dann etwas Zeit investieren, um sich wiederholende Aufgaben zu identifizieren und sich mit testbezogenen Tools für Ihren Tech-Stack vertraut zu machen.
Zur Inspiration finden Sie hier einige Tools, die Sie verwenden können:
- Testläufer. Achten Sie auf prägnante Syntax und Benutzerfreundlichkeit. Aus meiner Erfahrung empfehle ich für .NET xUnit (obwohl NUnit auch eine gute Wahl ist). Für JavaScript oder TypeScript gehe ich mit Jest. Versuchen Sie, die beste Übereinstimmung für Ihre Aufgaben und Ihre Denkweise zu finden, da sich Tools und Herausforderungen weiterentwickeln.
- Bibliotheken verspotten . Es kann Mocks auf niedriger Ebene für Codeabhängigkeiten wie Schnittstellen geben, aber es gibt auch Mocks auf höherer Ebene für Web-APIs oder Datenbanken. Für JavaScript und TypeScript sind in Jest enthaltene Mocks auf niedriger Ebene in Ordnung. Für .NET. Ich benutze Moq, obwohl NSubstitute auch großartig ist. Was Web-API-Mocks angeht, verwende ich gerne WireMock.NET. Es kann anstelle einer API verwendet werden, um die Behandlung von Antworten zu beheben und zu debuggen. Es ist auch in automatisierten Tests sehr zuverlässig und schnell. Datenbanken könnten mit ihren In-Memory-Pendants verspottet werden. EfCore in .NET bietet eine solche Option.
- Bibliotheken zur Datengenerierung . Diese Dienstprogramme füllen Ihre Datenobjekte mit Zufallsdaten. Sie sind nützlich, wenn Sie sich zum Beispiel nur um ein paar Felder aus einem großen Datentransferobjekt kümmern (wenn das so ist; vielleicht möchten Sie nur die Korrektheit der Zuordnung testen). Sie können sie für Tests und auch als Zufallsdaten verwenden, um sie in einem Formular anzuzeigen oder Ihre Datenbank zu füllen. Zu Testzwecken verwende ich AutoFixture in .NET.
- UI-Automatisierungsbibliotheken . Dies sind automatisierte Benutzer für automatisierte Tests: Sie können Ihre App ausführen, Formulare ausfüllen, auf Schaltflächen klicken, Beschriftungen lesen und so weiter. Um durch alle Elemente Ihrer App zu navigieren, müssen Sie sich nicht mit dem Klicken nach Koordinaten oder der Bilderkennung herumschlagen; Die wichtigsten Plattformen verfügen über die Werkzeuge, um benötigte Elemente nach Typ, Kennung oder Daten zu finden, sodass Sie Ihre Tests nicht bei jeder Neugestaltung ändern müssen. Sie sind robust, sobald Sie sie also für sich und CI zum Laufen gebracht haben (manchmal finden Sie heraus, dass Dinge nur auf Ihrem Computer funktionieren), werden sie weiter funktionieren. Ich verwende gerne FlaUI für .NET und Cypress für JavaScript und TypeScript.
- Assertion-Bibliotheken . Die meisten Testrunner enthalten Assertion-Tools, aber es gibt Fälle, in denen ein unabhängiges Tool Ihnen helfen kann, komplexe Assertionen mit einer saubereren und besser lesbaren Syntax zu schreiben, wie z. B. Fluent Assertions für .NET. Ich mag besonders die Funktion, um zu behaupten, dass Sammlungen unabhängig von der Reihenfolge eines Artikels oder seiner Adresse im Speicher gleich sind.
Möge der Fluss mit dir sein
Glück ist eng mit der sogenannten „Flow“-Erfahrung verbunden, die ausführlich im Buch „ Flow: The Psychology of Optimal Experience “ beschrieben wird. Um dieses Flow-Erlebnis zu erreichen, müssen Sie sich an einer Aktivität mit klaren Zielen beteiligen und Ihren Fortschritt sehen können. Aufgaben sollten zu sofortigem Feedback führen, wofür automatisierte Tests ideal sind. Sie müssen auch eine Balance zwischen Herausforderungen und Fähigkeiten finden, die jedem selbst überlassen ist. Tests, insbesondere wenn sie mit TDD angegangen werden, können Ihnen als Orientierungshilfe dienen und Vertrauen schaffen. Sie helfen Ihnen, sich konkrete Ziele zu setzen, wobei jeder bestandene Test ein Indikator für Ihren Fortschritt ist.
Der richtige Testansatz kann Sie glücklicher und produktiver machen, und Tests verringern die Burnout-Wahrscheinlichkeit. Der Schlüssel liegt darin, das Testen als Werkzeug (oder Toolset) zu betrachten, das Ihnen bei Ihrer täglichen Entwicklungsroutine helfen kann, und nicht als lästigen Schritt, um Ihren Code zukunftssicher zu machen.
Das Testen ist ein notwendiger Teil der Programmierung, der es Softwareentwicklern ermöglicht, ihre Arbeitsweise zu verbessern, die besten Ergebnisse zu liefern und ihre Zeit optimal zu nutzen. Vielleicht noch wichtiger ist, dass Tests Entwicklern helfen können, ihre Arbeit mehr zu genießen, und so ihre Moral und Motivation steigern.
