Schreiben von testbarem Code gegen Vermeiden spekulativer Allgemeinheit


11

Ich habe heute Morgen einige Blog-Beiträge gelesen und bin über diesen gestolpert :

Wenn die einzige Klasse, die jemals die Kundenschnittstelle implementiert, CustomerImpl ist, haben Sie nicht wirklich Polymorphismus und Substituierbarkeit, da es in der Praxis zur Laufzeit nichts zu ersetzen gibt. Es ist eine falsche Allgemeinheit.

Das macht für mich Sinn, da die Implementierung einer Schnittstelle die Komplexität erhöht und wenn es nur eine Implementierung gibt, könnte man argumentieren, dass sie unnötige Komplexität hinzufügt. Das Schreiben von Code, der abstrakter ist als er sein muss, wird oft als Codegeruch angesehen, der als "spekulative Allgemeinheit" bezeichnet wird (auch im Beitrag erwähnt).

Wenn ich jedoch TDD folge, kann ich ohne diese spekulative Allgemeinheit keine Testdoppel (einfach) erstellen, sei es in Form einer Schnittstellenimplementierung oder unserer anderen polymorphen Option, wodurch die Klasse vererbbar und ihre Methoden virtuell werden.

Wie bringen wir diesen Kompromiss in Einklang? Lohnt es sich, spekulativ allgemein zu sein, um das Testen / TDD zu erleichtern? Wenn Sie Test-Doubles verwenden, zählen diese als zweite Implementierungen und machen die Allgemeinheit somit nicht mehr spekulativ? Sollten Sie ein schwereres Mocking-Framework in Betracht ziehen, das das Verspotten konkreter Mitarbeiter ermöglicht (z. B. Moles versus Moq in der C # -Welt)? Oder sollten Sie mit den konkreten Klassen testen und so etwas wie "Integrationstests" schreiben, bis Ihr Design natürlich Polymorphismus erfordert?

Ich bin gespannt auf die Ansichten anderer Leute zu diesem Thema - vielen Dank im Voraus für Ihre Meinung.


Persönlich denke ich, dass Entitäten nicht verspottet werden sollten. Ich verspotte nur Dienste, und diese benötigen auf jeden Fall eine Schnittstelle, da der Code-Domänencode normalerweise keinen Verweis auf den Code enthält, in dem Dienste implementiert sind.
CodesInChaos

7
Wir Benutzer dynamisch getippter Sprachen lachen darüber, dass Sie sich über die Ketten lustig machen, die Ihre statisch getippten Sprachen Ihnen auferlegt haben. Dies ist eine Sache, die das Testen von Einheiten in dynamisch typisierten Sprachen erleichtert. Ich muss keine Schnittstelle entwickelt haben, um ein Objekt zu Testzwecken zu ersetzen.
Winston Ewert

Schnittstellen werden nicht nur verwendet, um die Allgemeinheit zu bewirken. Sie werden für viele Zwecke verwendet, wobei die Entkopplung Ihres Codes eine der wichtigsten ist. Das wiederum erleichtert das Testen Ihres Codes erheblich.
Marjan Venema

@WinstonEwert Das ist ein interessanter Vorteil des dynamischen Schreibens, den ich zuvor nicht als jemanden angesehen hatte, der, wie Sie betonen, normalerweise nicht in dynamisch getippten Sprachen funktioniert.
Erik Dietrich

@CodeInChaos Ich hatte die Unterscheidung für die Zwecke dieser Frage nicht berücksichtigt, obwohl es eine vernünftige Unterscheidung ist. In diesem Fall denken wir möglicherweise an Service- / Framework-Klassen mit nur einer (aktuellen) Implementierung. Angenommen, ich habe eine Datenbank, auf die ich mit DAOs zugreife. Sollte ich die DAOs erst anschließen, wenn ich ein sekundäres Persistenzmodell habe? (Das scheint der Autor des Blogposts zu sein)
Erik Dietrich

Antworten:


14

Ich habe den Blog-Beitrag gelesen und bin mit vielen Aussagen des Autors einverstanden. Wenn Sie jedoch sind Ihre Schreiben von Code - Schnittstellen für Einheit Testzwecke verwenden, würde ich sagen , dass die Pseudo-Implementierung der Schnittstelle ist Ihre zweite Implementierung. Ich würde argumentieren, dass es Ihrem Code wirklich nicht viel an Komplexität hinzufügt, insbesondere wenn der Kompromiss, dies nicht zu tun, dazu führt, dass Ihre Klassen eng miteinander verbunden und schwer zu testen sind.


3
Absolut richtig. Das Testen von Code ist Teil der Anwendung, da Sie ihn benötigen, um das Design, die Implementierung, die Wartung usw. richtig zu machen. Die Tatsache , dass Sie es nicht an den Kunden versenden , ist irrelevant - wenn es eine zweite Implementierung in Ihre Testsuite ist, dann ist die Allgemeinheit ist es , und Sie sollten es empfangen.
Kilian Foth

1
Dies ist die Antwort, die ich am überzeugendsten finde (und @KilianFoth fügt hinzu, dass es noch eine zweite Implementierung gibt, unabhängig davon, ob der Code ausgeliefert wird oder nicht). Ich werde die Antwort ein wenig akzeptieren, um zu sehen, ob sich noch jemand einmischt.
Erik Dietrich

Ich würde auch hinzufügen, wenn Sie von den Schnittstellen in den Tests abhängig sind, ist die Allgemeinheit nicht mehr spekulativ
Pete

"Nicht tun" (über Schnittstellen) führt nicht automatisch dazu, dass Ihre Klassen eng miteinander verbunden sind. Das tut es einfach nicht. In .NET Framework gibt es beispielsweise eine StreamKlasse, aber keine enge Kopplung.
Luke Puplett

3

Das Testen von Code im Allgemeinen ist nicht einfach. Wenn es so wäre, hätten wir das alles schon vor langer Zeit gemacht und erst in den letzten 10 bis 15 Jahren so viel daraus gemacht. Eine der größten Schwierigkeiten bestand immer darin, zu bestimmen, wie Code getestet werden soll, der zusammenhängend, gut faktorisiert und testbar ist, ohne die Kapselung zu unterbrechen. Das BDD-Prinzip schlägt vor, dass wir uns fast ausschließlich auf das Verhalten konzentrieren, und scheint in gewisser Weise darauf hinzudeuten, dass Sie sich nicht wirklich so sehr um die inneren Details kümmern müssen, aber dies kann es oft schwierig machen, die Dinge zu testen, wenn es welche gibt Viele private Methoden, die auf sehr versteckte Weise "Sachen" machen, da dies die Gesamtkomplexität Ihres Tests erhöhen kann, um alle möglichen Ergebnisse auf einer öffentlicheren Ebene zu behandeln.

Spott kann bis zu einem gewissen Grad helfen, ist aber wiederum ziemlich extern ausgerichtet. Die Abhängigkeitsinjektion kann auch sehr gut funktionieren, wiederum mit Mocks oder Test-Doubles. Dies kann jedoch auch erfordern, dass Sie Elemente entweder über eine Schnittstelle oder direkt verfügbar machen, die Sie sonst möglicherweise lieber versteckt hätten - dies gilt insbesondere, wenn Sie dies wünschen Haben Sie ein gutes paranoides Sicherheitsniveau für bestimmte Klassen in Ihrem System.

Für mich ist die Jury immer noch unschlüssig, ob Sie Ihre Klassen so gestalten sollen, dass sie leichter zu testen sind. Dies kann zu Problemen führen, wenn Sie neue Tests bereitstellen müssen, während der Legacy-Code beibehalten wird. Ich akzeptiere, dass Sie in der Lage sein sollten, absolut alles in einem System zu testen, aber ich mag es nicht, die privaten Interna einer Klasse - auch indirekt - freizulegen, nur damit ich einen Test für sie schreiben kann.

Für mich war die Lösung immer, einen ziemlich pragmatischen Ansatz zu wählen und eine Reihe von Techniken zu kombinieren, um sie an die jeweilige Situation anzupassen. Ich verwende viele geerbte Testdoppel, um innere Eigenschaften und Verhaltensweisen für meine Tests aufzudecken. Ich verspotte alles, was an meine Klassen angehängt werden kann, und wo dies die Sicherheit meiner Klassen nicht beeinträchtigt, werde ich ein Mittel bereitstellen, um Verhaltensweisen zum Testen zu überschreiben oder zu injizieren. Ich werde sogar in Betracht ziehen, eine ereignisgesteuerte Schnittstelle bereitzustellen, wenn dies dazu beiträgt, die Fähigkeit zum Testen von Code zu verbessern

Wo ich einen "nicht testbaren" Code finde , schaue ich nach, ob ich ihn umgestalten kann, um die Dinge testbarer zu machen. Wo viel privater Code hinter den Kulissen versteckt ist, finden Sie oft neue Klassen, die darauf warten, ausgebrochen zu werden. Diese Klassen können intern verwendet werden, können jedoch häufig unabhängig voneinander mit weniger privatem Verhalten und anschließend häufig weniger Zugriffs- und Komplexitätsebenen getestet werden. Eine Sache, die ich jedoch vermeiden möchte, ist das Schreiben von Produktionscode mit integriertem Testcode. Es kann verlockend sein, " Testfahnen " zu erstellen if testing then ..., die dazu führen, dass solche Schrecken eingeschlossen werden , die auf ein Testproblem hinweisen, das nicht vollständig dekonstruiert und unvollständig gelöst ist.

Es könnte hilfreich sein , das Buch xUnit Test Patterns von Gerard Meszaros zu lesen , in dem all diese Dinge ausführlicher behandelt werden, als ich hier behandeln kann. Ich mache wahrscheinlich nicht alles, was er vorschlägt, aber es hilft, einige der schwierigeren Testsituationen zu klären. Letztendlich möchten Sie in der Lage sein, Ihre Testanforderungen zu erfüllen, während Sie weiterhin Ihre bevorzugten Designs anwenden, und es hilft, alle Optionen besser zu verstehen, um besser entscheiden zu können, wo Sie möglicherweise Kompromisse eingehen müssen.


1

Hat die von Ihnen verwendete Sprache eine Möglichkeit, ein Objekt zum Testen zu "verspotten"? Wenn ja, können diese lästigen Schnittstellen verschwinden.

Aus einem anderen Grund kann es Gründe geben, ein SimpleInterface und ein einziges ComplexThing zu haben, das es implementiert. Es kann Teile des ComplexThing geben, auf die der SimpleInterface-Benutzer nicht zugreifen möchte. Es liegt nicht immer an einem überbordenden OO-ish-Codierer.

Ich werde jetzt zurücktreten und alle auf die Tatsache springen lassen, dass Code, der dies tut, für sie "schlecht riecht".


Ja, ich arbeite in Sprachen mit Verspottungs-Frameworks, die das Verspotten konkreter Objekte unterstützen. Diese Werkzeuge erfordern unterschiedliche Sprünge durch Reifen, um dies zu tun.
Erik Dietrich

0

Ich werde in zwei Teilen antworten:

  1. Sie benötigen keine Schnittstellen, wenn Sie nur am Testen interessiert sind. Zu diesem Zweck verwende ich Mocking-Frameworks (in Java: Mockito oder easymock). Ich glaube, dass der von Ihnen entworfene Code nicht zu Testzwecken geändert werden sollte. Das Schreiben von testbarem Code entspricht dem Schreiben von modularem Code, daher neige ich dazu, modularen (testbaren) Code zu schreiben und nur die öffentlichen Schnittstellen des Codes zu testen.

  2. Ich habe in einem großen Java-Projekt gearbeitet und bin zutiefst davon überzeugt, dass die Verwendung von schreibgeschützten (nur Getter-) Schnittstellen der richtige Weg ist (bitte beachten Sie, dass ich ein großer Fan von Unveränderlichkeit bin). Die implementierende Klasse verfügt möglicherweise über Setter, dies ist jedoch ein Implementierungsdetail, das keinen äußeren Schichten ausgesetzt werden sollte. Aus einer anderen Perspektive bevorzuge ich Komposition gegenüber Vererbung (Modularität, erinnerst du dich?), Daher helfen auch hier Schnittstellen. Ich bin bereit, die Kosten für spekulative Allgemeinheit zu bezahlen, als mich selbst in den Fuß zu schießen.


0

Ich habe viele Vorteile gesehen, seit ich angefangen habe, mehr auf eine Schnittstelle jenseits des Polymorphismus zu programmieren.

  • Es zwingt mich, vorher genauer über die Schnittstelle der Klasse (ihre öffentlichen Methoden) nachzudenken und darüber, wie sie mit den Schnittstellen anderer Klassen interagiert.
  • Es hilft mir, kleinere Klassen zu schreiben, die zusammenhängender sind und dem Prinzip der Einzelverantwortung folgen.
  • Es ist einfacher, meinen Code zu testen
  • Weniger statische Klassen / globaler Status als Klasse müssen auf Instanzebene sein
  • Einfachere Integration und Montage von Teilen, bevor das gesamte Programm fertig ist
  • Abhängigkeitsinjektion, die die Objektkonstruktion von der Geschäftslogik trennt

Viele Leute werden zustimmen, dass mehr, kleinere Klassen besser sind als weniger, größere Klassen. Sie müssen sich nicht so sehr auf einmal konzentrieren und jede Klasse hat einen genau definierten Zweck. Andere sagen vielleicht, dass Sie die Komplexität erhöhen, indem Sie mehr Klassen haben.

Es ist gut, Tools zu verwenden, um die Produktivität zu verbessern, aber ich denke, dass die Verwendung von Mock-Frameworks und dergleichen, anstatt Testbarkeit und Modularität direkt in den Code einzubauen, auf lange Sicht zu Code mit geringerer Qualität führen wird.

Alles in allem glaube ich, dass es mir geholfen hat, Code mit höherer Qualität zu schreiben, und die Vorteile überwiegen bei weitem alle Konsequenzen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.