Sollten wir unseren Code von Anfang an entwerfen, um Unit-Tests zu ermöglichen?

91

In unserem Team wird derzeit diskutiert, ob das Ändern des Code-Designs, um das Testen von Einheiten zu ermöglichen, ein Codegeruch ist oder inwieweit dies ohne Codegeruch möglich ist. Dies ist darauf zurückzuführen, dass wir gerade erst damit beginnen, Praktiken einzuführen, die in nahezu jedem anderen Softwareentwicklungsunternehmen vorhanden sind.

Insbesondere werden wir einen Web-API-Dienst haben, der sehr dünn sein wird. Seine Hauptaufgabe besteht darin, Webanforderungen / -antworten zusammenzustellen und eine zugrunde liegende API aufzurufen, die die Geschäftslogik enthält.

Ein Beispiel ist, dass wir eine Factory erstellen möchten, die einen Authentifizierungsmethodentyp zurückgibt. Wir müssen keine Schnittstelle erben, da wir nicht davon ausgehen, dass es jemals etwas anderes als die konkrete Art sein wird, die es sein wird. Zum Testen des Web-API-Dienstes müssen wir uns jedoch über diese Factory lustig machen.

Dies bedeutet im Wesentlichen, dass wir entweder die Web-API-Controller-Klasse so entwerfen, dass sie DI akzeptiert (über ihren Konstruktor oder Setter). Dies bedeutet, dass wir einen Teil des Controllers so entwerfen, dass DI zugelassen wird und eine Schnittstelle implementiert wird, die wir sonst nicht benötigen, oder dass wir sie verwenden ein Framework eines Drittanbieters wie Ninject, um zu vermeiden, dass der Controller auf diese Weise entworfen werden muss, aber wir müssen immer noch eine Schnittstelle erstellen.

Einige im Team scheinen nur zu Testzwecken ungern Code zu entwerfen. Es scheint mir, dass es Kompromisse geben muss, wenn Sie auf einen Unit-Test hoffen, aber ich bin mir nicht sicher, wie sie ihre Bedenken zerstreuen können.

Dies ist ein brandneues Projekt. Es geht also nicht wirklich darum, Code zu ändern, um Unit-Tests zu ermöglichen. Es geht darum, den Code, den wir schreiben werden, so zu gestalten, dass er für Einheiten testbar ist.

Lee
quelle
33
Lassen Sie mich das wiederholen: Ihre Kollegen möchten Unit-Tests für neuen Code, aber sie lehnen es ab, den Code so zu schreiben, dass er Unit-testbar ist, obwohl kein Risiko besteht, etwas Bestehendes zu beschädigen? Wenn dies zutrifft, sollten Sie die Antwort von @ KilianFoth akzeptieren und ihn bitten, den ersten Satz in seiner Antwort fett hervorzuheben! Ihre Kollegen haben anscheinend ein großes Missverständnis darüber, was ihre Arbeit ist.
Doc Brown
20
@Lee: Wer sagt, dass Entkopplung immer eine gute Idee ist? Haben Sie jemals eine Codebasis gesehen, in der alles als Schnittstelle übergeben wird, die mit einer Konfigurationsschnittstelle aus einer Schnittstellenfactory erstellt wurde? Ich habe; Es wurde in Java geschrieben und war ein komplettes, nicht zu wartendes, fehlerhaftes Durcheinander. Extreme Entkopplung ist Code-Verschleierung.
Christian Hackl
8
Michael Feathers ' Effective Working with Legacy Code (Effektives Arbeiten mit Legacy-Code) behandelt dieses Problem sehr gut und sollte Ihnen eine gute Vorstellung von den Vorteilen des Testens auch in einer neuen Codebasis geben.
30.
8
@ l0b0 Das ist so ziemlich die Bibel dafür. Auf stackexchange wäre es keine Antwort auf die Frage, aber in RL würde ich OP anweisen, dieses Buch (zumindest teilweise) lesen zu lassen. OP, erhält effektiv mit Legacy - Code arbeiten und lesen, zumindest teilweise (oder sagen Sie Ihrem Chef , es zu bekommen). Fragen wie diese werden angesprochen. Vor allem, wenn Sie nicht getestet haben und sich jetzt darauf einlassen - Sie haben vielleicht 20 Jahre Erfahrung, aber jetzt werden Sie Dinge tun , mit denen Sie keine Erfahrung haben . Es ist so viel einfacher, über sie zu lesen, als all das mühsam durch Ausprobieren zu lernen.
R. Schmitz
4
Vielen Dank für die Empfehlung von Michael Feathers Buch, ich werde auf jeden Fall ein Exemplar abholen.
Lee

Antworten:

204

Die Zurückhaltung, Code für Testzwecke zu ändern, zeigt, dass ein Entwickler die Rolle von Tests und implizit die eigene Rolle in der Organisation nicht verstanden hat.

Das Software-Geschäft dreht sich darum, eine Codebasis bereitzustellen, die geschäftlichen Wert schafft. Wir haben durch langjährige und erbitterte Erfahrung festgestellt, dass wir solche Codebasen von nicht-trivialer Größe nicht ohne Tests erstellen können. Daher sind Testsuiten ein wesentlicher Bestandteil des Geschäfts.

Viele Programmierer bekennen sich zu diesem Prinzip, akzeptieren es jedoch unbewusst nie. Es ist leicht zu verstehen, warum dies so ist; Das Bewusstsein, dass unsere eigenen mentalen Fähigkeiten nicht unendlich sind und in der Tat überraschend begrenzt sind, wenn wir mit der enormen Komplexität einer modernen Codebasis konfrontiert werden, ist unerwünscht und kann leicht unterdrückt oder rationalisiert werden. Die Tatsache, dass der Testcode nicht an den Kunden ausgeliefert wird, lässt vermuten, dass er ein Bürger zweiter Klasse ist und im Vergleich zum "wesentlichen" Geschäftscode nicht wesentlich ist. Und die Idee , dem Geschäftscode Testcode hinzuzufügen, wirkt sich für viele doppelt beleidigend aus.

Die Schwierigkeit, diese Praxis zu rechtfertigen, hat damit zu tun, dass das gesamte Bild der Wertschöpfung in einem Software-Geschäft oft nur von höheren Stellen in der Unternehmenshierarchie verstanden wird, aber diese Personen haben kein detailliertes technisches Verständnis dafür der Codierungsworkflow, der erforderlich ist, um zu verstehen, warum das Testen nicht abgeschafft werden kann. Deshalb werden sie zu oft von Praktizierenden befriedet, die ihnen versichern, dass das Testen im Allgemeinen eine gute Idee ist, aber "Wir sind Elite-Programmierer, die solche Krücken nicht brauchen", oder "dafür haben wir momentan keine Zeit", usw. usw. Die Tatsache, dass geschäftlicher Erfolg ein Spiel mit Zahlen ist und die Vermeidung von technischen Schulden Qualität etc. zeigt ihren Wert nur auf längere Sicht, was bedeutet, dass sie diesem Glauben oft sehr aufrichtig gegenüberstehen.

Kurz gesagt: Code testbar zu machen ist ein wesentlicher Bestandteil des Entwicklungsprozesses, nicht anders als in anderen Bereichen (viele Mikrochips sind nur zu Testzwecken mit einem erheblichen Anteil an Elementen entworfen ), aber es ist sehr leicht, die sehr guten Gründe dafür zu übersehen Das. Fallen Sie nicht in diese Falle.

Kilian Foth
quelle
39
Ich würde argumentieren, dass es von der Art der Änderung abhängt. Es gibt einen Unterschied zwischen der Vereinfachung des Testens von Code und der Einführung testspezifischer Hooks, die NIEMALS in der Produktion verwendet werden sollten. Ich persönlich bin vorsichtig mit letzterem, weil Murphy ...
Matthieu M.
61
Unit-Tests lösen häufig die Kapselung auf und machen den zu testenden Code komplexer als sonst erforderlich (z. B. durch Einführung zusätzlicher Schnittstellentypen oder Hinzufügen von Flags). Wie immer in der Softwareentwicklung hat jede gute Praxis und jede gute Regel ihren Anteil an den Verbindlichkeiten. Das blinde Produzieren vieler Komponententests kann sich nachteilig auf den Geschäftswert auswirken, ganz zu schweigen davon, dass das Schreiben und Verwalten der Tests bereits Zeit und Mühe kostet. Nach meiner Erfahrung haben Integrationstests einen viel größeren ROI und verbessern Software-Architekturen mit weniger Kompromissen.
Christian Hackl
20
@ Klar, aber Sie müssen überlegen, ob ein bestimmter Testtyp die Erhöhung der Codekomplexität rechtfertigt. Meine persönliche Erfahrung ist, dass Unit-Tests ein großartiges Werkzeug sind, bis zu dem Punkt, an dem grundlegende Designänderungen erforderlich sind, um das Verspotten zu berücksichtigen. Hier wechsle ich zu einer anderen Art von Tests. Das Schreiben von Komponententests auf Kosten einer wesentlich komplexeren Architektur, um Komponententests durchführen zu können, ist ein Blick auf den Bauchnabel.
Konrad Rudolph
21
@ChristianHackl warum sollte ein Komponententest die Kapselung brechen? Ich habe festgestellt, dass für den Code, an dem ich gearbeitet habe , das eigentliche Problem darin besteht, dass die zu testende Funktion überarbeitet werden muss, wenn zusätzliche Funktionen zum Aktivieren des Tests hinzugefügt werden müssen, sodass alle Funktionen gleich sind Abstraktionsebene (es sind Unterschiede in der Abstraktionsebene, die normalerweise das "Erfordernis" für zusätzlichen Code hervorrufen), wobei Code auf einer niedrigeren Ebene in ihre eigenen (testbaren) Funktionen verschoben wird.
Baldrickk
29
@ChristianHackl Unit-Tests sollten niemals die Kapselung unterbrechen. Wenn Sie versuchen, über einen Unit-Test auf private, geschützte oder lokale Variablen zuzugreifen, tun Sie dies falsch. Wenn Sie die Funktionalität von foo testen, testen Sie nur, ob sie tatsächlich funktioniert hat, und nicht, ob die lokale Variable x die Quadratwurzel der Eingabe y in der dritten Iteration der zweiten Schleife ist. Wenn einige Funktionen privat sind, testen Sie sie trotzdem transitiv. ob es wirklich groß und privat ist? Das ist ein Designfehler, aber wahrscheinlich nicht einmal außerhalb von C und C ++ mit Trennung der Header-Implementierung möglich.
opa
75

Es ist nicht so einfach, wie Sie vielleicht denken. Lassen Sie es uns aufschlüsseln.

  • Unit Tests zu schreiben ist definitiv eine gute Sache.

ABER!

  • Jede Änderung an Ihrem Code kann einen Fehler verursachen. Eine Änderung des Codes ohne guten Geschäftsgrund ist daher keine gute Idee.

  • Ihr 'sehr dünnes' Webapi scheint nicht der beste Fall für Unit-Tests zu sein.

  • Das gleichzeitige Ändern von Code und Tests ist eine schlechte Sache.

Ich würde den folgenden Ansatz vorschlagen:

  1. Schreiben Sie Integrationstests . Dies sollte keine Codeänderungen erfordern. Sie erhalten eine Übersicht über Ihre grundlegenden Testfälle und können überprüfen, ob weitere Codeänderungen keine Fehler verursachen.

  2. Stellen Sie sicher, dass neuer Code testbar ist und Unit- und Integrationstests enthält.

  3. Stellen Sie sicher, dass Ihre CI-Kette nach Builds und Bereitstellungen Tests ausführt.

Wenn Sie diese Dinge eingerichtet haben, sollten Sie erst darüber nachdenken, ältere Projekte auf ihre Testbarkeit hin zu überarbeiten.

Hoffentlich hat jeder aus dem Prozess Lehren gezogen und hat eine gute Vorstellung davon, wo das Testen am dringendsten erforderlich ist, wie Sie es strukturieren möchten und welchen Nutzen es für das Unternehmen bringt.

EDIT : Seit ich diese Antwort geschrieben habe, hat das OP die Frage geklärt, um zu zeigen, dass es sich um neuen Code handelt, nicht um Änderungen an vorhandenem Code. Ich dachte vielleicht naiv: "Ist der Gerätetest gut?" Streit wurde vor einigen Jahren beigelegt.

Es ist schwer vorstellbar, welche Codeänderungen für Komponententests erforderlich wären, aber keine allgemein bewährte Vorgehensweise, die Sie auf jeden Fall wünschen würden. Es wäre wahrscheinlich ratsam, die tatsächlichen Einwände zu untersuchen, möglicherweise ist es die Art des Unit-Tests, die beanstandet wird.

Ewan
quelle
12
Dies ist eine viel bessere Antwort als die akzeptierte. Das Stimmenungleichgewicht ist erschreckend.
Konrad Rudolph
4
@Lee Ein Komponententest sollte eine Funktionseinheit testen , die einer Klasse entsprechen kann oder nicht. Eine Funktionseinheit sollte an ihrer Schnittstelle (in diesem Fall möglicherweise die API) getestet werden. Das Testen kann Designgerüche hervorheben und die Notwendigkeit einer etwas anderen / stärkeren Nivellierung aufzeigen. Bauen Sie Ihre Systeme aus kleinen zusammensetzbaren Teilen auf. Sie sind einfacher zu überlegen und zu testen.
Wes Toleman
2
@KonradRudolph: Ich vermisse den Punkt, an dem das OP hinzufügte, dass es bei dieser Frage darum ging, neuen Code zu entwerfen, nicht den vorhandenen zu ändern. Es gibt also nichts zu brechen, weshalb die meisten dieser Antworten nicht zutreffen.
Doc Brown
1
Ich bin absolut nicht einverstanden mit der Aussage, dass das Schreiben von Unit-Tests immer eine gute Sache ist. Unit-Tests sind nur in einigen Fällen gut. Es ist albern, Unit-Tests zum Testen von Frontend-Code (UI-Code) zu verwenden. Sie dienen zum Testen der Geschäftslogik. Es ist auch gut, Komponententests zu schreiben, um fehlende Kompilierungsprüfungen (z. B. in Javascript) zu ersetzen. Der meiste Frontend-Code sollte ausschließlich End-to-End-Tests schreiben, keine Komponententests.
Sulthan,
1
Designs können definitiv unter "Testschaden" leiden. Normalerweise verbessert die Testbarkeit das Design: Sie stellen beim Schreiben von Tests fest, dass etwas nicht abgerufen werden kann, sondern übergeben werden muss, um klarere Schnittstellen usw. zu erhalten. Gelegentlich stolpern Sie jedoch nur zu Testzwecken über etwas, das ein unangenehmes Design erfordert . Ein Beispiel könnte ein reiner Testkonstruktor sein, der in Ihrem neuen Code aufgrund des vorhandenen Drittanbietercodes erforderlich ist, der beispielsweise ein Singleton verwendet. Wenn dies passiert: Machen Sie einen Schritt zurück und führen Sie nur einen Integrationstest durch, anstatt Ihr eigenes Design im Namen der Testbarkeit zu beschädigen.
Anders Forsgren
18

Das Entwerfen von Code als inhärent testbar ist kein Codegeruch. im Gegenteil, es ist das Zeichen eines guten Designs. Es gibt mehrere bekannte und weit verbreitete Entwurfsmuster (z. B. Model-View-Presenter), die ein einfaches (leichteres) Testen als großen Vorteil bieten.

Wenn Sie also eine Schnittstelle für Ihre konkrete Klasse schreiben müssen, um sie einfacher testen zu können, ist das eine gute Sache. Wenn Sie die konkrete Klasse bereits haben, können die meisten IDEs eine Schnittstelle daraus extrahieren, wodurch der erforderliche Aufwand minimal ist. Es ist ein bisschen mehr Arbeit, die beiden synchron zu halten, aber eine Benutzeroberfläche sollte sich sowieso nicht wesentlich ändern, und die Vorteile des Testens können diesen zusätzlichen Aufwand aufwiegen.

Andererseits als @MatthieuM. Erwähnt in einem Kommentar: Wenn Sie Ihrem Code bestimmte Einstiegspunkte hinzufügen, die in der Produktion niemals verwendet werden sollten, nur zum Testen, könnte dies ein Problem darstellen.

mmathis
quelle
Dieses Problem kann durch statische Code-Analyse gelöst werden - markieren Sie die Methoden (z. B. müssen sie benannt werden _ForTest) und überprüfen Sie die Codebasis auf Aufrufe von Nicht-Test-Code.
Riking
13

Es ist meiner Meinung nach sehr einfach zu verstehen, dass der zu testende Code zum Erstellen von Komponententests mindestens bestimmte Eigenschaften haben muss. Wenn der Code beispielsweise nicht aus einzelnen Einheiten besteht, die einzeln getestet werden können, ergibt das Wort "Einheitentest" nicht einmal einen Sinn. Wenn der Code diese Eigenschaften nicht hat, muss er zuerst geändert werden, das ist ziemlich offensichtlich.

Theoretisch kann man versuchen, zuerst eine testbare Code-Einheit zu schreiben, indem man alle SOLID-Prinzipien anwendet, und anschließend versuchen, einen Test dafür zu schreiben, ohne den ursprünglichen Code weiter zu modifizieren. Leider ist das Schreiben von Code, der wirklich in einer Einheit testbar ist, nicht immer kinderleicht. Es ist daher sehr wahrscheinlich, dass einige Änderungen erforderlich sind, die nur beim Erstellen der Tests erkannt werden. Dies gilt für Code, selbst wenn er mit dem Gedanken an Unit-Tests geschrieben wurde, und auf jeden Fall mehr für Code, der geschrieben wurde, als "Unit-Testbarkeit" am Anfang nicht auf der Tagesordnung stand.

Es gibt einen bekannten Ansatz, der versucht, das Problem zu lösen, indem zuerst die Komponententests geschrieben werden. Er heißt Test Driven Development (TDD) und kann sicherlich dazu beitragen, den Code von Anfang an testbarer zu machen.

Natürlich tritt die Zurückhaltung, Code später zu ändern, um ihn testbar zu machen, häufig in einer Situation auf, in der der Code zuerst manuell getestet wurde und / oder in der Ausführung einwandfrei funktioniert, so dass das Ändern tatsächlich neue Fehler hervorrufen kann, das stimmt. Der beste Ansatz, um dies zu verringern, besteht darin, zuerst eine Regressionstestsuite zu erstellen (die häufig mit nur sehr geringen Änderungen an der Codebasis implementiert werden kann) sowie andere begleitende Maßnahmen wie Codeüberprüfungen oder neue manuelle Testsitzungen. Das sollten Sie tun, um sicherzugehen, dass die Neugestaltung einiger Interna nichts Wichtiges kaputt macht.

Doc Brown
quelle
Interessant, dass Sie TDD erwähnen. Wir versuchen, BDD / TDD einzuführen, das ebenfalls auf Widerstand gestoßen ist - nämlich was "der Mindestcode, der weitergegeben werden muss", wirklich bedeutet.
Lee
2
@Lee: Veränderungen in eine Organisation zu bringen, verursacht immer einen gewissen Widerstand, und es braucht immer etwas Zeit, um sich an neue Dinge anzupassen, das ist keine neue Weisheit. Das ist ein Menschenproblem.
Doc Brown
Absolut. Ich wünschte nur, wir hätten mehr Zeit!
Lee
Es geht häufig darum , den Leuten zu zeigen , dass sie dadurch Zeit sparen (und hoffentlich auch schnell). Warum etwas tun, das dir nicht nützt?
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen: Das Team kann dem OP ebenfalls zeigen, dass seine Vorgehensweise Zeit spart. Wer weiß? Aber ich frage mich, ob wir hier eigentlich nicht mit weniger technischen Problemen konfrontiert sind. Das OP kommt immer wieder hierher, um uns mitzuteilen, was sein Team falsch macht (seiner Meinung nach), als würde er versuchen, Verbündete für seine Sache zu finden. Es kann vorteilhafter sein, das Projekt gemeinsam mit dem Team und nicht mit Fremden auf Stack Exchange zu diskutieren .
Christian Hackl
11

Ich bezweifle Ihre (unbegründete) Behauptung:

Um den Web-API-Service zu testen, müssen wir uns über diese Factory lustig machen

Das ist nicht unbedingt wahr. Es gibt viele Möglichkeiten, Tests zu schreiben, und es gibt Möglichkeiten, Komponententests zu schreiben, die keine Verspottungen beinhalten. Noch wichtiger ist, dass es andere Arten von Tests gibt, beispielsweise Funktions- oder Integrationstests. Oft ist es möglich, eine "Testnaht" an einer "Schnittstelle" zu finden, die keine OOP-Programmiersprache ist interface.

Einige Fragen, die Ihnen helfen sollen, eine alternative Testnaht zu finden, die natürlicher sein könnte:

  • Will ich jemals eine Thin-Web-API über eine andere API schreiben ?
  • Kann ich die Codeduplizierung zwischen der Web-API und der zugrunde liegenden API reduzieren? Kann man in Bezug auf die anderen generiert werden?
  • Kann ich die gesamte Web-API und die zugrunde liegende API als eine einzige "Black-Box" -Einheit behandeln und aussagekräftige Aussagen über das Verhalten des Ganzen treffen?
  • Wie würden wir vorgehen, wenn die Web-API in Zukunft durch eine neue Implementierung ersetzt werden müsste?
  • Wenn die Web-API in Zukunft durch eine neue Implementierung ersetzt würde, könnten Clients der Web-API dies bemerken? Wenn das so ist, wie?

Eine weitere unbegründete Behauptung, die Sie aufstellen, betrifft DI:

Entweder entwerfen wir die Web-API-Controller-Klasse so, dass sie DI akzeptiert (über ihren Konstruktor oder Setter). Das bedeutet, wir entwerfen einen Teil des Controllers, um DI zuzulassen und eine Schnittstelle zu implementieren, die wir ansonsten nicht benötigen, oder wir verwenden einen Drittanbieter Framework wie Ninject, um zu vermeiden, dass der Controller auf diese Weise entworfen werden muss, aber wir müssen noch eine Schnittstelle erstellen.

Abhängigkeitsinjektion bedeutet nicht notwendigerweise, eine neue zu erstellen interface. Zum Beispiel wegen eines Authentifizierungstokens: Können Sie ein echtes Authentifizierungstoken einfach programmgesteuert erstellen ? Dann kann der Test solche Token erzeugen und injizieren. Hängt der Prozess zur Validierung eines Tokens von einem kryptografischen Geheimnis ab? Ich hoffe, Sie haben kein Geheimnis fest codiert - ich würde erwarten, dass Sie es irgendwie aus dem Speicher lesen können, und in diesem Fall können Sie einfach ein anderes (bekanntes) Geheimnis in Ihren Testfällen verwenden.

Dies soll nicht heißen, dass Sie niemals ein neues erstellen sollten interface. Aber lassen Sie sich nicht darauf fixieren, dass es nur eine Möglichkeit gibt, einen Test zu schreiben oder ein Verhalten vorzutäuschen. Wenn Sie über den Tellerrand hinaus denken, finden Sie normalerweise eine Lösung, die ein Minimum an Codeverfälschungen erfordert und Ihnen dennoch den gewünschten Effekt verleiht.

Daniel Pryden
quelle
Ich beziehe mich auf die Behauptungen in Bezug auf Schnittstellen, aber selbst wenn wir sie nicht verwenden würden, müssten wir Objekte auf irgendeine Weise injizieren, dies ist die Sorge des Restes des Teams. Das heißt, einige im Team würden sich über eine parameterlose Steuerung freuen, die die konkrete Implementierung instanziiert und dabei belässt. Tatsächlich vertrat ein Mitglied die Idee, Reflektion zum Einfügen von Mocks zu verwenden, damit wir keinen Code entwerfen müssen, um sie zu akzeptieren. Welches ist ein reeky Code Geruch imo
Lee
9

Sie haben Glück, denn dies ist ein neues Projekt. Ich habe festgestellt, dass Test Driven Design sehr gut zum Schreiben von gutem Code geeignet ist (weshalb wir es in erster Linie tun).

Indem Sie im Voraus herausfinden , wie ein bestimmtes Stück Code mit realistischen Eingabedaten aufgerufen und dann realistische Ausgabedaten abgerufen werden, die Sie wie beabsichtigt überprüfen können, erstellen Sie das API-Design sehr früh im Prozess und haben eine gute Chance, eine zu erhalten Nützliches Design, da Sie nicht durch vorhandenen Code behindert werden, der für die Anpassung neu geschrieben werden muss. Außerdem ist es für Ihre Kollegen einfacher zu verstehen, sodass Sie frühzeitig wieder gute Diskussionen führen können.

Beachten Sie, dass "nützlich" im obigen Satz nicht nur bedeutet, dass die resultierenden Methoden leicht aufzurufen sind, sondern auch, dass Sie dazu neigen, saubere Schnittstellen zu erhalten, die sich in Integrationstests leicht aufrüsten lassen und für die Sie Modelle schreiben können.

Denke darüber nach. Vor allem mit Peer Review. Meiner Erfahrung nach macht sich der Aufwand sehr schnell bezahlt.

Thorbjørn Ravn Andersen
quelle
Wir haben auch ein Problem mit TDD, nämlich was "Minimum Code to Pass" ausmacht. Ich habe dem Team diesen Prozess demonstriert und sie haben Ausnahme gemacht, nicht nur das zu schreiben, was wir bereits entworfen haben - was ich verstehen kann. Das "Minimum" scheint nicht definiert zu sein. Wenn wir einen Test schreiben und klare Pläne und Entwürfe haben, warum nicht das schreiben, um den Test zu bestehen?
Lee
@Lee "minimum code to pass" ... nun, das klingt vielleicht ein bisschen dumm, aber es ist buchstäblich das, was es sagt. Wenn Sie beispielsweise einen Test haben UserCanChangeTheirPassword, rufen Sie im Test die (noch nicht vorhandene) Funktion auf, um das Kennwort zu ändern, und bestätigen dann, dass das Kennwort tatsächlich geändert wurde. Dann schreiben Sie die Funktion, bis Sie den Test ausführen können und er weder Ausnahmen auslöst noch eine falsche Behauptung hat. Wenn Sie zu diesem Zeitpunkt einen Grund haben, Code hinzuzufügen, wird dieser Grund in einen anderen Test übernommen, z UserCantChangePasswordToEmptyString.
R. Schmitz
@Lee Letztendlich werden Ihre Tests die Dokumentation dessen sein, was Ihr Code tut, außer es ist eine Dokumentation, die prüft, ob es selbst erfüllt ist, anstatt nur Tinte auf Papier zu sein. Vergleichen Sie auch mit dieser Frage - Eine Methode CalculateFactorial, die nur 120 zurückgibt und den Test besteht. Das ist das Minimum. Es ist auch offensichtlich nicht das, was beabsichtigt war, aber das nur bedeutet , dass Sie einen anderen Test benötigen um auszudrücken , was wurde gedacht.
R. Schmitz
1
@Lee Kleine Schritte. Das absolute Minimum kann mehr sein, als Sie denken, wenn der Code über das Nebensächliche steigt. Auch das Design, das Sie bei der Implementierung des Ganzen auf einmal machen, ist möglicherweise wieder weniger optimal, da Sie Annahmen darüber treffen, wie es gemacht werden soll, ohne die Tests geschrieben zu haben, die dies demonstrieren. Denken Sie erneut daran, dass der Code zunächst fehlschlagen sollte.
Thorbjørn Ravn Andersen
1
Auch Regressionstests sind sehr wichtig. Sind sie in Reichweite für das Team?
Thorbjørn Ravn Andersen
8

Wenn Sie den Code ändern müssen, ist dies der Codegeruch.

Aus persönlicher Erfahrung ist mein Code schlecht, wenn es schwierig ist, Tests für ihn zu schreiben. Es ist kein schlechter Code, weil er nicht wie geplant ausgeführt wird oder funktioniert. Es ist schlecht, weil ich nicht so schnell verstehen kann, warum er funktioniert. Wenn ich auf einen Fehler stoße, weiß ich, dass es lange dauern wird, bis ich ihn behebe. Es ist auch schwierig / unmöglich, den Code wiederzuverwenden.

Guter (sauberer) Code unterteilt Aufgaben in kleinere Abschnitte, die auf einen Blick leicht zu verstehen sind (oder zumindest gut aussehen). Das Testen dieser kleineren Abschnitte ist einfach. Ich kann auch Tests schreiben, die nur dann einen Teil der Codebasis mit ähnlicher Leichtigkeit testen, wenn ich mir der Unterabschnitte ziemlich sicher bin (hier hilft auch die Wiederverwendung, da sie bereits getestet wurde).

Halten Sie den Code von Anfang an einfach zu testen, zu überarbeiten und wiederzuverwenden, und Sie werden sich nicht selbst umbringen, wenn Sie Änderungen vornehmen müssen.

Ich schreibe dies, während ich ein Projekt, das ein wegwerfbarer Prototyp sein sollte, vollständig in saubereren Code umbaue. Es ist viel besser, es von Anfang an richtig zu machen und schlechten Code so schnell wie möglich umzugestalten, als stundenlang auf einen Bildschirm zu starren und Angst zu haben, irgendetwas zu berühren, aus Angst, etwas zu beschädigen, das teilweise funktioniert.

David
quelle
3
"Throwaway-Prototyp" - jedes einzelne Projekt beginnt sein Leben als eines davon ... am besten, wenn man die Dinge so betrachtet, als wäre es niemals so. schreibe das wie ich bin .. weißt du was? ... einen Wegwerf-Prototyp umgestalten, der sich als ungeeignet herausstellte;)
Algy Taylor
4
Wenn Sie sicher sein möchten, dass ein Wegwerfprototyp weggeworfen wird, schreiben Sie ihn in einer Prototypsprache, die in der Produktion niemals zulässig ist. Clojure und Python sind eine gute Wahl.
Thorbjørn Ravn Andersen
2
@ ThorbjørnRavnAndersen Das hat mich zum Lachen gebracht. Wollte das eine Ausgrabung in diesen Sprachen sein? :)
Lee
@Lee. Nein, nur Beispiele für Sprachen, die für die Produktion möglicherweise nicht akzeptabel sind - normalerweise, weil niemand in der Organisation sie verwalten kann, weil sie mit ihnen nicht vertraut sind und ihre Lernkurven steil sind. Wenn diese akzeptabel sind, wählen Sie eine andere, die dies nicht ist.
Thorbjørn Ravn Andersen
4

Ich würde argumentieren, dass das Schreiben von Code, der nicht Unit-getestet werden kann, ein Codegeruch ist. Wenn Ihr Code nicht Unit-getestet werden kann, ist er im Allgemeinen nicht modular, was das Verstehen, Verwalten oder Verbessern erschwert. Handelt es sich bei dem Code möglicherweise um Glue-Code, der nur im Hinblick auf Integrationstests sinnvoll ist, können Sie Integrationstests durch Komponententests ersetzen. Aber selbst dann, wenn die Integration fehlschlägt, müssen Sie das Problem eingrenzen, und Komponententests sind ein guter Weg, dies zu erreichen Tu es.

Du sagst

Wir planen die Erstellung einer Factory, die einen Authentifizierungsmethodentyp zurückgibt. Wir müssen keine Schnittstelle erben, da wir nicht davon ausgehen, dass es jemals etwas anderes als die konkrete Art sein wird, die es sein wird. Zum Testen des Web-API-Dienstes müssen wir uns jedoch über diese Factory lustig machen.

Ich folge dem nicht wirklich. Der Grund für eine Fabrik, die etwas erstellt, besteht darin, dass Sie Fabriken oder das, was die Fabrik erstellt, problemlos ändern können, sodass andere Teile des Codes nicht geändert werden müssen. Wenn sich Ihre Authentifizierungsmethode niemals ändern wird, ist die Werkseinstellung unbrauchbar. Wenn Sie jedoch im Test eine andere Authentifizierungsmethode als in der Produktion verwenden möchten, ist eine Factory, die im Test eine andere Authentifizierungsmethode als in der Produktion zurückgibt, eine gute Lösung.

Sie brauchen dafür weder DI noch Mocks. Sie müssen lediglich die verschiedenen Authentifizierungstypen in Ihrer Factory unterstützen und diese irgendwie konfigurieren, z. B. anhand einer Konfigurationsdatei oder einer Umgebungsvariablen.

Alter Pro
quelle
2

In jeder Ingenieurdisziplin, die mir einfällt, gibt es nur einen Weg, um ein angemessenes oder höheres Qualitätsniveau zu erreichen:

Zur Berücksichtigung von Inspektionen / Tests im Design.

Dies gilt für Konstruktion, Chip-Design, Software-Entwicklung und Fertigung. Dies bedeutet nicht, dass das Testen die Säule ist, auf der jedes Design aufgebaut sein muss, und überhaupt nicht. Bei jeder Konstruktionsentscheidung müssen die Konstrukteure jedoch die Auswirkungen auf die Testkosten klar erkennen und bewusste Entscheidungen über den Kompromiss treffen.

In einigen Fällen sind manuelle oder automatisierte Tests (z. B. Selen) praktischer als Komponententests, bieten aber auch eine akzeptable Testabdeckung für sich. In seltenen Fällen kann es auch akzeptabel sein, etwas fast Ungetestetes hinauszuwerfen. Diese müssen jedoch von Fall zu Fall bewusst sein. Das Aufrufen eines Designs, bei dem ein "Codegeruch" getestet wird, weist auf einen schwerwiegenden Mangel an Erfahrung hin.

Peter
quelle
1

Ich habe festgestellt, dass Unit-Tests (und andere Arten von automatisierten Tests) dazu neigen, Code-Gerüche zu reduzieren, und ich kann mir kein einziges Beispiel vorstellen, in dem sie Code-Gerüche einführen. Unit-Tests zwingen Sie normalerweise dazu, besseren Code zu schreiben. Wenn Sie eine im Test befindliche Methode nicht problemlos verwenden können, warum sollte es in Ihrem Code einfacher sein?

Gut geschriebene Unit-Tests zeigen Ihnen, wie der Code verwendet werden soll. Sie sind eine Form von ausführbarer Dokumentation. Ich habe abscheulich geschriebene, zu lange Komponententests gesehen, die einfach nicht verstanden werden konnten. Schreiben Sie diese nicht! Wenn Sie lange Tests schreiben müssen, um Ihre Klassen einzurichten, müssen Ihre Klassen überarbeitet werden.

Unit-Tests werden aufzeigen, wo einige Ihrer Code-Gerüche sind. Ich würde empfehlen, Michael C. Feathers ' Arbeiten mit Legacy Code effektiv zu lesen . Auch wenn Ihr Projekt neu ist, müssen Sie möglicherweise einige nicht offensichtliche Techniken anwenden, um den Code ordnungsgemäß zu testen, wenn es noch keine (oder viele) Komponententests enthält.

CJ Dennis
quelle
3
Sie können versucht sein, viele Indirektionsebenen einzuführen, um testen zu können, und diese dann nie wie erwartet zu verwenden.
Thorbjørn Ravn Andersen
1

In einer Nussschale:

Testbarer Code ist (normalerweise) wartbarer Code - oder besser gesagt, schwer zu testender Code ist normalerweise schwer zu warten. Designing Code, der nicht prüfbar ist verwandt mit der Gestaltung einer Maschine, die nicht reparierbar ist - schade , dass der arme shmuck , die wird es zugeordnet zu reparieren schließlich (es kann ihnen sein).

Ein Beispiel ist, dass wir eine Factory erstellen möchten, die einen Authentifizierungsmethodentyp zurückgibt. Wir müssen keine Schnittstelle erben, da wir nicht davon ausgehen, dass es jemals etwas anderes als die konkrete Art sein wird, die es sein wird.

Sie wissen, dass Sie in drei Jahren fünf verschiedene Arten von Authentifizierungsmethoden benötigen werden, jetzt, wo Sie das gesagt haben, richtig? Die Anforderungen ändern sich, und während Sie ein Überarbeiten Ihres Designs vermeiden sollten, bedeutet ein testbares Design, dass Ihr Design (gerade) genug Nähte hat, um ohne (zu viel) Schmerz geändert zu werden - und dass die Modultests Ihnen automatisierte Mittel bieten, um dies zu erkennen Ihre Änderungen machen nichts kaputt.

CharonX
quelle
1

Das Entwerfen mit Abhängigkeitsinjektion ist kein Codegeruch - es ist eine bewährte Methode. Die Verwendung von DI dient nicht nur der Testbarkeit. Wenn Sie Ihre Komponenten auf DI aufbauen, werden Modularität und Wiederverwendbarkeit unterstützt. Dies erleichtert das Auslagern wichtiger Komponenten (z. B. einer Datenbankschnittstellenschicht). Durch die richtige Ausführung wird zwar ein gewisses Maß an Komplexität hinzugefügt, die Trennung der Ebenen und die Isolation der Funktionen werden jedoch erleichtert, sodass die Komplexität einfacher verwaltet und navigiert werden kann. Dies erleichtert die ordnungsgemäße Überprüfung des Verhaltens der einzelnen Komponenten, wodurch Fehler reduziert und das Aufspüren von Fehlern vereinfacht werden.

Zenilogix
quelle
1
"richtig gemacht" ist ein Problem. Ich muss zwei Projekte unterhalten, bei denen DI falsch gemacht wurde (obwohl ich darauf abzielte, es "richtig" zu machen). Dies macht den Code einfach schrecklich und viel schlimmer als die Legacy-Projekte ohne DI und Unit-Tests. DI richtig hinzubekommen ist nicht einfach.
Jan
@Jan das ist interessant. Wie haben sie es falsch gemacht?
Lee
1
@Lee One-Projekt ist ein Dienst, der eine schnelle Startzeit benötigt, aber beim Start sehr langsam ist, da die gesamte Klasseninitialisierung im Voraus vom DI-Framework (Castle Windsor in C #) durchgeführt wird. Ein weiteres Problem, das ich in diesen Projekten sehe, ist das Verwechseln von DI mit der Erstellung von Objekten mit "new", wobei die DI umgangen wird. Das macht das Testen wieder schwierig und führte zu einigen schlechten Rennbedingungen.
Jan
1

Dies bedeutet im Wesentlichen, dass wir entweder die Web-API-Controller-Klasse so entwerfen, dass sie DI akzeptiert (über ihren Konstruktor oder Setter). Dies bedeutet, dass wir einen Teil des Controllers so entwerfen, dass DI zugelassen wird und eine Schnittstelle implementiert wird, die wir sonst nicht benötigen, oder dass wir sie verwenden ein Framework eines Drittanbieters wie Ninject, um zu vermeiden, dass der Controller auf diese Weise entworfen werden muss, aber wir müssen immer noch eine Schnittstelle erstellen.

Schauen wir uns den Unterschied zwischen einem testbaren an:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

und nicht testbarer Controller:

public class MyController : Controller
{
}

Die erste Option enthält buchstäblich 5 zusätzliche Codezeilen, von denen zwei von Visual Studio automatisch generiert werden können. Sobald Sie Ihr Abhängigkeitsinjektionsframework so eingerichtet haben, dass es IMyDependencyzur Laufzeit einen konkreten Typ ersetzt - was für jedes anständige DI-Framework eine weitere einzelne Codezeile darstellt -, funktioniert alles, außer dass Sie jetzt Ihren Controller nach Herzenslust verspotten und testen können .

6 zusätzliche Codezeilen, um die Testbarkeit zu gewährleisten ... und Ihre Kollegen argumentieren, das sei "zu viel Arbeit"? Dieses Argument passt nicht zu mir und sollte auch nicht zu dir passen.

Und Sie müssen keine Schnittstelle zum Testen erstellen und implementieren: Mit Moq können Sie beispielsweise das Verhalten eines konkreten Typs zu Zwecken des Komponententests simulieren. Das wird Ihnen natürlich nicht viel nützen, wenn Sie diese Typen nicht in die Klassen einspeisen können, die Sie testen.

Die Abhängigkeitsinjektion ist eines der Dinge, die Sie sich fragen, wenn Sie sie erst einmal verstanden haben: "Wie habe ich ohne sie gearbeitet?". Es ist einfach, es ist effektiv und es macht nur Sinn. Bitte lassen Sie nicht zu, dass das mangelnde Verständnis Ihrer Kollegen für neue Dinge die Testbarkeit Ihres Projekts beeinträchtigt.

Ian Kemp
quelle
1
Was Sie so schnell als "Unverständnis für Neues" abtun, kann sich als gutes Verständnis für Altes herausstellen. Abhängigkeitsinjektion ist sicherlich nicht neu. Die Idee und wahrscheinlich die frühesten Implementierungen sind Jahrzehnte alt. Und ja, ich glaube, Ihre Antwort ist ein Beispiel für Code, der durch Unit-Tests komplizierter wird, und möglicherweise ein Beispiel für Unit-Tests, die die Kapselung aufheben (denn wer sagt, dass die Klasse überhaupt einen öffentlichen Konstruktor hat?). Ich habe oft die Abhängigkeitsinjektion aus Codebasen entfernt, die ich aufgrund der Kompromisse von jemand anderem geerbt hatte.
Christian Hackl
Controller haben immer einen öffentlichen Konstruktor, implizit oder nicht, da MVC dies erfordert. "Kompliziert" - vielleicht, wenn Sie nicht verstehen, wie Konstruktoren funktionieren. Encapsulation - ja, in einigen Fällen, aber die Debatte zwischen DI und Encapsulation ist eine andauernde, höchst subjektive Debatte, die hier nicht weiterhilft. Insbesondere für die meisten Anwendungen wird DI Ihnen besser dienen als Encapsulation IMO.
Ian Kemp
In Bezug auf öffentliche Konstrukteure: In der Tat ist dies eine Besonderheit des verwendeten Frameworks. Ich dachte an den allgemeineren Fall einer gewöhnlichen Klasse, die nicht durch ein Framework instanziiert wird. Warum bedeutet es Ihrer Meinung nach ein Unverständnis darüber, wie Konstruktoren funktionieren, wenn zusätzliche Methodenparameter als zusätzliche Komplexität betrachtet werden? Ich weiß jedoch zu schätzen, dass Sie die Existenz eines Kompromisses zwischen DI und Kapselung anerkennen.
Christian Hackl
0

Wenn ich Unit-Tests schreibe, beginne ich zu überlegen, was in meinem Code schief gehen könnte. Es hilft mir, das Code-Design zu verbessern und das Single-Responsibility-Prinzip (SRP) anzuwenden . Wenn ich einige Monate später wieder zurückkomme, um denselben Code zu ändern, kann ich feststellen, dass die vorhandene Funktionalität nicht beschädigt ist.

Es gibt einen Trend, reine Funktionen so oft wie möglich zu verwenden (serverlose Apps). Unit-Tests helfen mir, Zustände zu isolieren und reine Funktionen zu schreiben.

Insbesondere werden wir einen Web-API-Dienst haben, der sehr dünn sein wird. Seine Hauptaufgabe besteht darin, Webanforderungen / -antworten zusammenzustellen und eine zugrunde liegende API aufzurufen, die die Geschäftslogik enthält.

Schreiben Sie zuerst Komponententests für die zugrunde liegende API. Wenn Sie genügend Entwicklungszeit haben, müssen Sie auch Tests für den Thin-Web-API-Service schreiben.

TL; DR, Komponententests tragen zur Verbesserung der Codequalität bei und tragen dazu bei, zukünftige Änderungen am Code ohne Risiko durchzuführen. Es verbessert auch die Lesbarkeit des Codes. Verwenden Sie Tests anstelle von Kommentaren, um Ihren Standpunkt zu verdeutlichen.

Ashutosh
quelle
0

Das Fazit und was sollte Ihr Argument mit dem zögernden Los sein, ist, dass es keinen Konflikt gibt. Der große Fehler scheint gewesen zu sein, dass jemand die Idee geprägt hat, Menschen, die das Testen hassen, "zum Testen zu designen". Sie hätten einfach den Mund halten oder es anders ausdrücken sollen, wie "Nehmen wir uns die Zeit, dies richtig zu machen".

Die Idee, dass "Sie eine Schnittstelle implementieren müssen", um etwas testbar zu machen, ist falsch. Das Interface ist bereits implementiert, es ist nur noch nicht in der Klassendeklaration deklariert. Es geht darum, vorhandene öffentliche Methoden zu erkennen, ihre Signaturen in eine Schnittstelle zu kopieren und diese Schnittstelle in der Klassendeklaration zu deklarieren. Keine Programmierung, keine Änderungen an der vorhandenen Logik.

Anscheinend haben einige Leute eine andere Vorstellung davon. Ich schlage vor, Sie versuchen, dies zuerst zu beheben.

Martin Maat
quelle