Ich habe gelesen, dass die Verwendung von "new" in einem Konstruktor (für andere Objekte als einfache Werte) eine schlechte Praxis ist, da dies das Testen von Einheiten unmöglich macht (da diese Mitbearbeiter ebenfalls erstellt werden müssen und nicht verspottet werden können). Da ich keine wirklichen Erfahrungen mit Unit-Tests habe, versuche ich, einige Regeln zu sammeln, die ich zuerst lernen werde. Ist dies auch eine allgemein gültige Regel, unabhängig von der verwendeten Sprache?
unit-testing
constructors
Ezoela Vacca
quelle
quelle
new
bedeutet verschiedene Dinge in verschiedenen Sprachen.new
Schlüsselwort. Nach welchen Sprachen fragst du?Antworten:
Es gibt immer Ausnahmen, und ich habe Probleme mit dem "immer" im Titel, aber ja, diese Richtlinie ist allgemein gültig und gilt auch außerhalb des Konstruktors.
Die Verwendung von new in einem Konstruktor verletzt das D in SOLID (Prinzip der Abhängigkeitsinversion). Es erschwert das Testen Ihres Codes, da es beim Unit-Testen nur um Isolation geht. Es ist schwierig, Klassen zu isolieren, wenn sie konkrete Referenzen haben.
Es geht aber nicht nur um Unit-Tests. Was ist, wenn ich ein Repository auf zwei verschiedene Datenbanken gleichzeitig verweisen möchte? Die Möglichkeit, in meinem eigenen Kontext zu übergeben, ermöglicht es mir, zwei verschiedene Repositorys zu instanziieren, die auf unterschiedliche Speicherorte verweisen.
Wenn Sie new nicht im Konstruktor verwenden, wird der Code flexibler. Dies gilt auch für Sprachen, die andere Konstrukte als die
new
Objektinitialisierung verwenden können.Es ist jedoch klar, dass Sie ein gutes Urteilsvermögen verwenden müssen. Es gibt viele Fälle, in denen die Verwendung in Ordnung
new
ist oder in denen es besser ist, dies nicht zu tun, aber Sie werden keine negativen Konsequenzen haben. Irgendwann muss irgendwonew
angerufen werden. Seien Sie nur vorsichtig, wenn Sienew
eine Klasse aufrufen , von der viele andere Klassen abhängen.Das Initialisieren einer leeren Privatsammlung in Ihrem Konstruktor ist in Ordnung und das Injizieren wäre absurd.
Je mehr Verweise eine Klasse darauf hat, desto sorgfältiger sollten Sie sein, nicht
new
aus ihr heraus aufzurufen .quelle
head = null
der Code in irgendeiner Weise verbessert wurde? Warum ist es besser, eingeschlossene Sammlungen als null zu belassen und sie bei Bedarf zu erstellen, als dies im Konstruktor zu tun?Obwohl ich es vorziehen würde, den Konstruktor nur zum Initialisieren der neuen Instanz zu verwenden, anstatt mehrere andere Objekte zu erstellen, sind Hilfsobjekte in Ordnung, und Sie müssen beurteilen, ob etwas ein solcher interner Helfer ist oder nicht.
Wenn die Klasse eine Auflistung darstellt, verfügt sie möglicherweise über ein internes Hilfsarray, eine Liste oder ein Hashset. Es würde verwendet
new
, um diese Helfer zu erstellen, und es würde als ganz normal angesehen. Die Klasse bietet keine Injektion an, um verschiedene interne Helfer zu verwenden, und hat keinen Grund dazu. In diesem Fall möchten Sie die öffentlichen Methoden des Objekts testen, die möglicherweise zum Akkumulieren, Entfernen und Ersetzen von Elementen in der Auflistung führen.In gewissem Sinne ist das Klassenkonstrukt einer Programmiersprache ein Mechanismus zum Erzeugen von Abstraktionen höherer Ebenen, und wir erzeugen solche Abstraktionen, um die Lücke zwischen der Problemdomäne und den Programmiersprachenprimitiven zu schließen. Der Klassenmechanismus ist jedoch nur ein Werkzeug. Es variiert je nach Programmiersprache und für einige Domänenabstraktionen sind in einigen Sprachen einfach mehrere Objekte auf der Ebene der Programmiersprache erforderlich.
Zusammenfassend muss beurteilt werden, ob für die Abstraktion lediglich ein oder mehrere interne / Hilfsobjekte erforderlich sind, während der Aufrufer sie weiterhin als einzelne Abstraktion betrachtet, oder ob die anderen Objekte für den Aufrufer besser zugänglich sind, für den sie erstellt werden sollen Steuerung von Abhängigkeiten, die beispielsweise vorgeschlagen werden, wenn der Aufrufer diese anderen Objekte bei der Verwendung der Klasse sieht.
quelle
Nicht alle Mitarbeiter sind interessant genug, um sie einzeln zu testen. Sie können sie (indirekt) durch die Hosting- / Instanziierungsklasse testen. Dies entspricht möglicherweise nicht der Vorstellung einiger Leute, dass sie jede Klasse, jede öffentliche Methode usw. testen müssen, insbesondere wenn sie danach testen. Wenn Sie TDD verwenden, können Sie diesen "Collaborator" überarbeiten, indem Sie eine Klasse extrahieren, deren Test bereits vollständig abgeschlossen ist.
quelle
Not all collaborators are interesting enough to unit-test separately
Ende der Geschichte :-), Dieser Fall ist möglich und niemand wagte es zu erwähnen. @Joppe Ich ermutige Sie, die Antwort ein wenig auszuarbeiten. Beispielsweise könnten Sie einige Beispiele für Klassen hinzufügen, die lediglich Implementierungsdetails darstellen (die nicht zum Ersetzen geeignet sind), und wie sie später extrahiert werden können, wenn wir dies für erforderlich halten.new File()
etwas dateibezogenen zu tun, gibt es keinen Sinn, diesen Anruf zu verbieten. Was werden Sie tun, um Regressionstests für das stdlib-File
Modul zu schreiben ? Unwahrscheinlich. Andererseits ist es zweifelhafter ,new
eine selbst erfundene Klasse anzurufen.new File()
.Achten Sie darauf, "Regeln" für Probleme zu lernen, auf die Sie noch nie gestoßen sind. Wenn Sie auf eine "Regel" oder "Best Practice" stoßen, würde ich vorschlagen, ein einfaches Spielzeugbeispiel dafür zu finden, wo diese Regel "verwendet" werden soll, und zu versuchen, dieses Problem selbst zu lösen, und dabei zu ignorieren, was die "Regel" sagt.
In diesem Fall könnten Sie versuchen, zwei oder drei einfache Klassen und einige Verhaltensweisen zu finden, die sie implementieren sollten. Implementieren Sie die Klassen so, wie es sich für Sie natürlich anfühlt, und schreiben Sie für jedes Verhalten einen Komponententest. Erstellen Sie eine Liste der aufgetretenen Probleme, z. B. wenn Sie anfingen, auf eine Weise zu arbeiten, und diese später ändern mussten. wenn Sie sich nicht sicher sind, wie die Dinge zusammenpassen sollen; wenn Sie sich darüber geärgert haben, Kesselschild zu schreiben; etc.
Dann versuchen , das gleiche Problem zu lösen , indem Sie die „Regel“. Erstellen Sie erneut eine Liste der aufgetretenen Probleme. Vergleichen Sie die Listen und überlegen Sie, welche Situationen besser sind, wenn Sie die Regel befolgen, und welche nicht.
Was Ihre eigentliche Frage betrifft , neige ich dazu, einen Port- und Adapter- Ansatz zu bevorzugen , bei dem zwischen "Kernlogik" und "Diensten" unterschieden wird (dies ähnelt der Unterscheidung zwischen reinen Funktionen und effektiven Prozeduren).
Bei der Kernlogik geht es darum, Dinge "innerhalb" der Anwendung basierend auf der Problemdomäne zu berechnen. Es könnte enthält Klassen wie
User
,Document
,Order
,Invoice
, etc. Es ist in Ordnung zu haben Kernklassen rufennew
für andere Kern - Klassen, da sie „intern“ Implementierungsdetails. Wenn Sie beispielsweise eineOrder
erstellen, können Sie auch eineInvoice
und eineDocument
Detaillierung der Bestellung erstellen . Es ist nicht notwendig, diese während der Tests zu verspotten, da dies die tatsächlichen Dinge sind, die wir testen möchten!Über die Ports und Adapter interagiert die Kernlogik mit der Außenwelt. Dies ist , wo Dinge wie
Database
,ConfigFile
,EmailSender
usw. leben. Dies sind die Dinge, die das Testen schwierig machen. Daher ist es ratsam, diese außerhalb der Kernlogik zu erstellen und nach Bedarf weiterzugeben (entweder mit Abhängigkeitsinjektion oder als Methodenargumente usw.).Auf diese Weise kann die Kernlogik (der anwendungsspezifische Teil, in dem sich die wichtige Geschäftslogik befindet und der die meisten Änderungen unterworfen sind) selbstständig getestet werden, ohne sich um Datenbanken, Dateien, E-Mails usw. kümmern zu müssen. Wir können nur einige Beispielwerte übergeben und überprüfen, ob wir die richtigen Ausgabewerte erhalten.
Die Ports und Adapter können separat mit Hilfe von Mocks für die Datenbank, das Dateisystem usw. getestet werden, ohne sich um die Geschäftslogik kümmern zu müssen. Wir können nur einige Beispielwerte übergeben und sicherstellen, dass sie gespeichert / gelesen / gesendet / usw. werden. passend.
quelle
Lassen Sie mich die Frage beantworten und zusammenfassen, was ich hier für die wichtigsten Punkte halte. Ich werde einige Benutzer der Kürze halber zitieren.
Ja, die Verwendung von
new
Innenkonstruktoren führt häufig zu Konstruktionsfehlern (z. B. dichter Kopplung), die unsere Konstruktion starr machen. Schwer zu testen , aber nicht unmöglich. Die Eigenschaft, die hier im Spiel ist, ist die Belastbarkeit (Toleranz gegenüber Änderungen) 1 .Trotzdem stimmt das obige Zitat nicht immer. In einigen Fällen kann es Klassen geben , die eng miteinander verbunden sein sollen . David Arno hat ein paar kommentiert.
Genau. Einige Klassen (zum Beispiel innere Klassen) können lediglich Implementierungsdetails der Hauptklasse sein. Diese sollen zusammen mit der Hauptklasse getestet werden und sind nicht unbedingt austauschbar oder erweiterbar.
Wenn wir diese Klassen durch unseren SOLID- Kult extrahieren , verletzen wir möglicherweise ein anderes gutes Prinzip. Das sogenannte Gesetz von Demeter . Was ich andererseits aus gestalterischer Sicht sehr wichtig finde.
Die wahrscheinliche Antwort hängt also , wie üblich, ab . Die Verwendung von
new
Inside-Konstruktoren kann eine schlechte Praxis sein. Aber nicht immer systematisch.Wir müssen also bewerten, ob es sich bei den Klassen um Implementierungsdetails der Hauptklasse handelt (in den meisten Fällen nicht). Wenn ja, lassen Sie sie in Ruhe. Wenn dies nicht der Fall ist , ziehen Sie Techniken wie Composition Root oder Dependency Injection von IoC-Containern in Betracht .
1: Das Hauptziel von SOLID ist es nicht, unseren Code testbarer zu machen. Es soll unseren Code toleranter für Änderungen machen. Flexibler und damit einfacher zu testen
Anmerkung: David Arno, TheWhisperCat, ich hoffe es macht Ihnen nichts aus, dass ich Sie zitiert habe.
quelle
Betrachten Sie als einfaches Beispiel den folgenden Pseudocode
Da dies
new
ein reines Implementierungsdetail vonFoo
und beides istFoo::Bar
und einFoo::Baz
Teil davon istFoo
, hat es beim Testen von EinheitenFoo
keinen Sinn, Teile von zu verspottenFoo
. Sie verspotten die Teile nur im Freien,Foo
wenn Sie die Einheit testenFoo
.quelle
Ja, die Verwendung von "new" in Ihren Anwendungsstammklassen ist ein Codegeruch. Dies bedeutet, dass Sie die Klasse an die Verwendung einer bestimmten Implementierung binden und keine andere ersetzen können. Entscheiden Sie sich immer für das Einfügen der Abhängigkeit in den Konstruktor. Auf diese Weise können Sie während des Testens nicht nur leicht verspottete Abhängigkeiten einfügen, sondern Ihre Anwendung wird auch viel flexibler, da Sie bei Bedarf schnell verschiedene Implementierungen austauschen können.
BEARBEITEN: Für Abwähler - hier ist ein Link zu einem Softwareentwicklungsbuch, das "neu" als möglichen Codegeruch kennzeichnet: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false
quelle
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.
Warum ist das ein Problem? Nicht jede einzelne Abhängigkeit in einem Abhängigkeitsbaum solltenew
Schlüsselworts ist keine schlechte Praxis und hat es auch nie getan . Es ist , wie Sie das Tool , dass Angelegenheiten verwenden. Sie verwenden beispielsweise keinen Vorschlaghammer, bei dem ein Kugelhammer ausreicht.