TDD: Eine Entwicklungsmethode, die mein Leben verändert hat

Um 7:15 Uhr Unser technischer Support ist mit Arbeit überschwemmt. Good Morning America hat gerade über uns gesprochen und viele derjenigen, die unsere Website zum ersten Mal besuchen, sind auf Fehler gestoßen.

Wir haben einen echten Ansturm. Bevor wir die Gelegenheit verlieren, die Besucher der Ressource in neue Benutzer umzuwandeln, werden wir das Fixpack einführen. Einer der Entwickler hat etwas vorbereitet. Er glaubt, dass dies helfen wird, das Problem zu bewältigen. Wir setzen einen Link zur aktualisierten Version des Programms, das noch nicht in Produktion gegangen ist, zum Chat des Unternehmens und bitten alle, es zu testen. Es funktioniert!

Unsere heldenhaften Ingenieure führen Skripte aus, um die Systeme bereitzustellen, und nach einigen Minuten geht das Update in den Kampf. Plötzlich verdoppelt sich die Anzahl der Anrufe beim technischen Support. Unsere dringende Lösung hat etwas kaputt gemacht, die Entwickler haben sich die Schuld gegeben, und die Ingenieure haben das System auf den vorherigen Stand zurückgesetzt.

Bild

Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, glaubt, dass all dies dank TDD hätte vermieden werden können.

Warum verwende ich TDD?


Ich war schon lange nicht mehr in solchen Situationen. Und es ist nicht so, dass die Entwickler aufgehört haben, Fehler zu machen. Tatsache ist, dass seit vielen Jahren in jedem Team, das ich leitete und beeinflusste, die TDD-Methodik angewendet wurde. Natürlich treten immer noch Fehler auf, aber das Eindringen in die Produktion von Problemen, die ein Projekt „zum Erliegen bringen“ können, ist auf nahezu Null gesunken, obwohl die Häufigkeit von Software-Updates und die Anzahl der Aufgaben, die während des Updates gelöst werden müssen, seitdem exponentiell gestiegen sind als etwas passierte, über das ich am Anfang gesprochen habe.

Wenn mich jemand fragt, warum er TDD kontaktieren soll, erzähle ich ihm diese Geschichte und ich kann mich an ein Dutzend ähnlicher Fälle erinnern. Einer der wichtigsten Gründe, warum ich zu TDD gewechselt bin, ist, dass diese Methode die Abdeckung von Tests mit Code verbessert , was zu 40-80% weniger Fehlern in der Produktion führt. Das gefällt mir an TDD am besten. Dies nimmt den Entwicklern einen Berg von Problemen ab.

Darüber hinaus ist anzumerken, dass TDD Entwicklern die Angst vor Änderungen am Code erspart.

In Projekten, an denen ich teilnehme, verhindern Sätze von automatischen Modul- und Funktionstests fast täglich, dass Code in die Produktion gelangt, was die Arbeit dieser Projekte ernsthaft stören kann. Zum Beispiel sehe ich mir jetzt 10 automatische Bibliotheksaktualisierungen an, die letzte Woche vorgenommen wurden, z. B. bevor sie ohne Verwendung von TDD veröffentlicht wurden. Ich hätte Angst, dass sie etwas ruinieren könnten.

Alle diese Updates wurden automatisch in den Code integriert und werden bereits in der Produktion verwendet. Ich habe keine von ihnen manuell überprüft und mir überhaupt keine Sorgen gemacht, dass sie sich negativ auf das Projekt auswirken könnten. Gleichzeitig musste ich nicht lange nachdenken, um dieses Beispiel zu geben. Ich habe gerade GitHub geöffnet, mir die jüngsten Fusionen angesehen und gesehen, wovon ich sprach. Die Aufgabe, die zuvor manuell gelöst wurde (oder, noch schlimmer, das Problem, das ignoriert wurde), ist jetzt ein automatisierter Hintergrundprozess. Sie können versuchen, etwas Ähnliches ohne gute Codeabdeckung mit Tests zu tun, aber ich würde dies nicht empfehlen.

Was ist TDD?


TDD steht für Test Driven Development. Der durch Anwendung dieser Methodik implementierte Prozess ist sehr einfach:


Tests erkennen Fehler, Tests werden erfolgreich abgeschlossen, Refactoring wird durchgeführt

Hier sind die Grundprinzipien für die Verwendung von TDD:

  1. Bevor Sie einen Implementierungscode für eine Funktion schreiben, schreiben sie einen Test, mit dem Sie überprüfen können, ob dieser zukünftige Implementierungscode funktioniert oder nicht. Bevor Sie mit dem nächsten Schritt fortfahren, wird der Test gestartet und davon überzeugt, dass ein Fehler ausgegeben wird. Dank dessen können Sie sicher sein, dass der Test keine falsch positiven Ergebnisse liefert, sondern eine Art Test der Tests selbst ist.
  2. Sie erstellen eine Implementierung der Opportunity und stellen sicher, dass der Test erfolgreich bestanden wird.
  3. Führen Sie gegebenenfalls ein Code-Refactoring durch. Das Refactoring bei Vorhandensein eines Tests, der dem Entwickler anzeigen kann, ob das System ordnungsgemäß oder falsch funktioniert, gibt dem Entwickler Vertrauen in seine Aktionen.

Wie kann TDD helfen, die für die Entwicklung von Programmen erforderliche Zeit zu sparen?


Auf den ersten Blick scheint es, dass das Schreiben von Tests eine signifikante Erhöhung der Menge an Projektcode bedeutet und dass all dies den Entwicklern viel zusätzliche Zeit kostet. In meinem Fall war zunächst alles genau das, und ich versuchte zu verstehen, wie es im Prinzip möglich ist, den getesteten Code zu schreiben und dem bereits geschriebenen Code Tests hinzuzufügen.

TDD ist durch eine bestimmte Lernkurve gekennzeichnet, und während ein Anfänger entlang dieser Kurve klettert, kann sich die für die Entwicklung erforderliche Zeit um 15-35% erhöhen. Oft passiert genau das. Aber ungefähr zwei Jahre nach dem Beginn der Verwendung von TDD beginnt etwas Unglaubliches zu passieren. Ich begann zum Beispiel mit dem vorläufigen Schreiben von Komponententests und programmierte schneller als zuvor, wenn TDD nicht verwendet wurde.

Vor einigen Jahren habe ich im Client-System die Möglichkeit implementiert, mit Fragmenten eines Videoclips zu arbeiten. Der Punkt war nämlich, dass es dem Benutzer möglich sein würde, den Anfang und das Ende des Aufzeichnungsfragments anzugeben und einen Link dazu zu erhalten, der es ermöglichen würde, auf eine bestimmte Stelle im Clip und nicht auf diesen gesamten Clip zu verweisen.

Ich habe nicht gearbeitet. Der Spieler erreichte das Ende des Fragments und spielte es weiter, aber ich hatte keine Ahnung, warum das so war.

Ich nahm an, dass das Problem darin bestand, Ereignis-Listener nicht ordnungsgemäß zu verbinden. Mein Code sah ungefähr so ​​aus:

video.addEventListener('timeupdate', () => {  if (video.currentTime >= clip.stopTime) {    video.pause();  } }); 

Das Auffinden des Problems sah folgendermaßen aus: Änderungen vornehmen, kompilieren, neu starten, klicken, warten ... Diese Abfolge von Aktionen wurde immer wieder wiederholt.

Um jede der in das Projekt eingeführten Änderungen zu überprüfen, dauerte es fast eine Minute, und ich hatte unglaublich viele Möglichkeiten, das Problem zu lösen (die meisten davon 2-3 Mal).

Vielleicht habe ich einen Fehler im Schlüsselwort timeupdate ? Habe ich die Funktionen der korrekten Arbeit mit der API verstanden? Funktioniert der Aufruf von video.pause() ? Ich nahm Änderungen am Code vor, fügte console.log() , kehrte zum Browser zurück, klickte auf die Schaltfläche console.log() , klickte auf die Position am Ende des ausgewählten Fragments und wartete geduldig, bis der Clip vollständig abgespielt war. Die Protokollierung im if Konstrukt führte zu nichts. Es sah aus wie ein Hinweis auf ein mögliches Problem. Ich habe das Wort timeupdate aus der API-Dokumentation kopiert, um absolut sicher zu sein, dass ich bei der Eingabe keinen Fehler gemacht habe. Ich lade die Seite erneut, klicke erneut und warte erneut. Und wieder weigert sich das Programm, richtig zu arbeiten.

Ich habe schließlich console.log() außerhalb des if Blocks platziert. "Es wird nicht helfen", dachte ich. Am Ende war die if so einfach, dass ich einfach keine Ahnung hatte, wie ich sie falsch buchstabieren sollte. Aber die Anmeldung in diesem Fall hat funktioniert. Ich verschluckte mich an Kaffee. "Was zum Teufel ist das?" Dachte ich.
Murphys Debugging-Gesetz. Der Ort des Programms, den Sie nie getestet haben, da Sie fest davon überzeugt sind, dass es keine Fehler enthalten kann, wird sich als genau der Ort herausstellen, an dem Sie einen Fehler finden, nachdem Sie nach vollständiger Erschöpfung nur deshalb Änderungen an diesem Ort vorgenommen haben dass sie bereits alles versucht haben, was sie sich vorstellen können.

Ich habe im Programm einen Haltepunkt gesetzt, um zu verstehen, was passiert. Ich habe die Bedeutung von clip.stopTime . Zu meiner Überraschung war es undefined . Warum? Ich sah mir den Code noch einmal an. Wenn der Benutzer die Endzeit des Fragments auswählt, platziert das Programm die Markierung für das Ende des Fragments an der richtigen Stelle, setzt jedoch nicht den Wert von clip.stopTime . "Ich bin ein unglaublicher Idiot", dachte ich, "ich darf erst am Ende meines Lebens Computer benutzen."

Ich habe das und Jahre später nicht vergessen. Und alles - dank der Sensation, die ich erlebt habe und die immer noch einen Fehler gefunden hat. Sie wissen wahrscheinlich, wovon ich spreche. Mit all dem ist passiert. Und vielleicht kann sich jeder in diesem Mem wiedererkennen.


So sehe ich aus, wenn ich programmiere

Wenn ich dieses Programm heute schreiben würde, würde ich so daran arbeiten:

 describe('clipReducer/setClipStopTime', async assert => { const stopTime = 5; const clipState = {   startTime: 2,   stopTime: Infinity }; assert({   given: 'clip stop time',   should: 'set clip stop time in state',   actual: clipReducer(clipState, setClipStopTime(stopTime)),   expected: { ...clipState, stopTime } }); }); 

Es besteht das Gefühl, dass es viel mehr Code als in dieser Zeile gibt:

 clip.stopTime = video.currentTime 

Aber das ist der springende Punkt. Dieser Code dient als Spezifikation. Dies ist sowohl eine Dokumentation als auch ein Beweis dafür, dass der Code den Anforderungen dieser Dokumentation entspricht. Und da diese Dokumentation vorhanden ist, muss ich mir keine Sorgen machen, ob ich bei der Einführung dieser Änderungen mit der Endzeit des Clips gegen die korrekte Operation verstoßen habe, wenn ich die Reihenfolge der Arbeit mit dem Marker für die Endzeit eines Fragments ändere.

Hier ist übrigens nützliches Material zum Schreiben von Komponententests, genau wie das, das wir uns gerade angesehen haben.

Es geht nicht darum, wie lange es dauert, diesen Code einzugeben. Der Punkt ist, wie lange das Debuggen dauert, wenn etwas schief geht. Wenn der Code falsch ist, liefert der Test einen hervorragenden Fehlerbericht. Ich werde sofort wissen, dass das Problem nicht der Event-Handler ist. Ich werde wissen, dass es entweder in setClipStopTime() oder in clipReducer() , wo eine clipReducer() implementiert ist. Dank des Tests würde ich wissen, welche Funktionen der Code ausführt, was er tatsächlich anzeigt und was von ihm erwartet wird. Und was noch wichtiger ist, mein Kollege wird das gleiche Wissen haben, der sechs Monate, nachdem ich den Code geschrieben habe, neue Funktionen einführen wird.

Beim Starten eines neuen Projekts habe ich als erstes ein Beobachter-Skript eingerichtet , das bei jeder Änderung einer bestimmten Datei automatisch Komponententests ausführt. Ich programmiere oft mit zwei Monitoren. Auf einer davon wird die Entwicklerkonsole geöffnet, in der die Ergebnisse eines solchen Skripts angezeigt werden, auf der anderen Seite wird die Schnittstelle der Umgebung angezeigt, in der ich den Code schreibe. Wenn ich den Code ändere, finde ich normalerweise innerhalb von 3 Sekunden heraus, ob die Änderung funktioniert hat oder nicht.

TDD ist für mich viel mehr als nur eine Versicherung. Dies ist die Möglichkeit, ständig und schnell in Echtzeit Informationen über den Status meines Codes zu erhalten. Sofortige Belohnung in Form von bestandenen Tests oder eine sofortige Fehlermeldung für den Fall, dass ich etwas falsch gemacht habe.

Wie hat mir die TDD-Methode beigebracht, wie man besseren Code schreibt?


Ich möchte eine Aufnahme machen, gebe sogar zu, dass es peinlich ist: Ich hatte keine Ahnung, wie man Anwendungen erstellt, bevor ich TDD und Unit-Tests gelernt habe. Ich kann mir nicht vorstellen, wie ich überhaupt eingestellt wurde, aber nachdem ich viele hundert Entwickler interviewt habe, kann ich zuversichtlich sagen, dass sich viele Programmierer in einer ähnlichen Situation befinden. Die TDD-Methodik hat mir fast alles beigebracht, was ich über die effiziente Zerlegung und Zusammensetzung von Softwarekomponenten weiß (ich meine Module, Funktionen, Objekte, Komponenten der Benutzeroberfläche usw.).

Der Grund dafür ist, dass Komponententests den Programmierer zwingen, Komponenten isoliert voneinander und von den E / A-Subsystemen zu testen. Wenn das Modul mit einigen Eingabedaten versehen ist, muss es bestimmte, zuvor bekannte Ausgabedaten ausgeben. Wenn er dies nicht tut, schlägt der Test fehl. Wenn dies der Fall ist, ist der Test erfolgreich. Der Punkt hier ist, dass das Modul unabhängig vom Rest der Anwendung arbeiten sollte. Wenn Sie die Logik des Status testen, sollten Sie dies tun können, ohne etwas auf dem Bildschirm anzuzeigen oder etwas in der Datenbank zu speichern. Wenn Sie die Bildung der Benutzeroberfläche testen, sollten Sie sie testen können, ohne die Seite in einen Browser laden oder auf Netzwerkressourcen zugreifen zu müssen.

Unter anderem hat mich die TDD-Methodik gelehrt, dass das Leben viel einfacher wird, wenn Sie bei der Entwicklung von Benutzeroberflächenkomponenten nach Minimalismus streben. Darüber hinaus sollten Geschäftslogik und Nebenwirkungen von der Benutzeroberfläche isoliert werden. Aus praktischer Sicht bedeutet dies, dass es bei Verwendung eines komponentenbasierten UI-Frameworks wie React oder Angular ratsam sein kann, Präsentationskomponenten zu erstellen, die für die Anzeige von etwas auf dem Bildschirm verantwortlich sind, und Containerkomponenten, die nicht miteinander verbunden sind sind gemischt.

Eine Präsentationskomponente, die bestimmte Eigenschaften erhält, generiert immer das gleiche Ergebnis. Solche Komponenten können mithilfe von Komponententests leicht überprüft werden. Auf diese Weise können Sie herausfinden, ob die Komponente mit den Eigenschaften korrekt funktioniert und ob bestimmte bei der Bildung der Schnittstelle verwendete bedingte Logik korrekt ist. Beispielsweise ist es möglich, dass die Komponente, aus der die Liste besteht, nur eine Aufforderung zum Hinzufügen eines neuen Elements zur Liste anzeigt, wenn die Liste leer ist.

Ich wusste über das Prinzip der Aufgabentrennung Bescheid, lange bevor ich TDD beherrschte, aber ich wusste nicht, wie ich die Verantwortung zwischen verschiedenen Einheiten teilen sollte.

Durch Unit-Tests konnte ich die Verwendung von Mokas zum Testen von Objekten untersuchen. Dann stellte ich fest, dass das Rauchen ein Zeichen dafür ist, dass möglicherweise etwas mit dem Code nicht stimmt . Es hat mich verblüfft und meine Herangehensweise an die Software-Komposition komplett verändert.

Die gesamte Softwareentwicklung ist eine Komposition: Der Prozess, große Probleme in viele kleine, leicht zu lösende Probleme aufzuteilen und dann Lösungen für diese Probleme zu erstellen, die die Anwendung bilden. Tuxing zum Zwecke von Einheitentests zeigt an, dass die atomaren Einheiten der Zusammensetzung tatsächlich nicht atomar sind. Durch das Studium, wie man Mok loswird, ohne die Codeabdeckung durch Tests zu beeinträchtigen, konnte ich lernen, wie man die unzähligen versteckten Gründe für die starke Verbundenheit von Entitäten identifiziert.

Dadurch konnte ich als Entwickler professionell wachsen. Dies brachte mir bei, wie man viel einfacheren Code schreibt, der einfacher zu erweitern, zu warten und zu skalieren ist. Dies gilt für die Komplexität des Codes selbst und für die Organisation seiner Arbeit in großen verteilten Systemen wie Cloud-Infrastrukturen.

Wie spart TDD Teamzeit?


Ich habe bereits gesagt, dass TDD in erster Linie zu einer verbesserten Codeabdeckung mit Tests führt. Der Grund dafür ist, dass wir erst mit dem Schreiben von Code zum Implementieren einer Funktion beginnen, wenn wir einen Test schreiben, der die korrekte Funktionsweise dieses zukünftigen Codes überprüft. Zuerst schreiben wir einen Test. Dann lassen wir es mit einem Fehler enden. Dann schreiben wir den Code zur Implementierung der Opportunity. Wir testen den Code, erhalten eine Fehlermeldung, erreichen das korrekte Bestehen der Tests, führen Refactoring durch und wiederholen diesen Vorgang.

Mit diesem Vorgang können Sie einen "Zaun" erstellen, durch den nur sehr wenige Fehler "springen" können. Dieser Fehlerschutz hat erstaunliche Auswirkungen auf das gesamte Entwicklungsteam. Es lindert die Angst vor dem Merge-Team.

Die hohe Codeabdeckung mit Tests ermöglicht es dem Team, den Wunsch zu beseitigen, jede kleine Änderung der Codebasis manuell zu steuern. Codeänderungen werden zu einem natürlichen Bestandteil des Workflows.

Die Angst vor Änderungen am Code loszuwerden, ähnelt der Unschärfe einer bestimmten Maschine. Andernfalls stoppt die Maschine schließlich - bis sie geschmiert und neu gestartet wird.

Ohne diese Angst ist die Arbeit an Programmen viel ruhiger als zuvor. Pull-Anfragen werden nicht bis zum letzten verzögert. Das CI / CD-System führt die Tests aus. Wenn die Tests fehlschlagen, wird der Prozess zum Vornehmen von Änderungen am Projektcode gestoppt. Gleichzeitig werden Fehlermeldungen und Informationen darüber, wo genau sie aufgetreten sind, nur schwer nicht bemerkt.

Das ist der springende Punkt.

Liebe Leser! Verwenden Sie TDD, wenn Sie an Ihren Projekten arbeiten?

Source: https://habr.com/ru/post/de450316/


All Articles