Bei den gängigsten Sprachen (Java, C #, Java usw.) scheint es manchmal so, als würden Sie im Widerspruch zur Sprache arbeiten, wenn Sie Ihren Code vollständig TDD-fähig machen möchten.
In Java und C # möchten Sie beispielsweise alle Abhängigkeiten Ihrer Klassen verspotten, und die meisten Verspottungs-Frameworks empfehlen, dass Sie Schnittstellen und keine Klassen verspotten. Dies bedeutet häufig, dass Sie viele Schnittstellen mit einer einzigen Implementierung haben (dieser Effekt macht sich noch deutlicher bemerkbar, da TDD Sie zwingt, eine größere Anzahl kleinerer Klassen zu schreiben). Lösungen, mit denen Sie konkrete Klassen richtig verspotten können, können beispielsweise den Compiler ändern oder Klassenlader usw. überschreiben, was ziemlich unangenehm ist.
Wie würde eine Sprache aussehen, wenn sie von Grund auf neu für TDD entwickelt würde? Möglicherweise eine Möglichkeit auf Sprachebene, Abhängigkeiten zu beschreiben (anstatt Schnittstellen an einen Konstruktor zu übergeben) und die Schnittstelle einer Klasse zu trennen, ohne dies explizit zu tun?
Antworten:
Vor vielen Jahren habe ich einen Prototyp zusammengestellt, der sich mit einer ähnlichen Frage befasste. Hier ist ein Screenshot:
Die Idee war, dass die Aussagen mit dem Code selbst übereinstimmen und alle Tests grundsätzlich bei jedem Tastendruck ausgeführt werden. Sobald Sie den Test bestanden haben, wird die Methode grün.
quelle
Es wäre eher dynamisch als statisch typisiert. Das Entenschreiben würde dann den gleichen Job machen wie Schnittstellen in statisch typisierten Sprachen. Außerdem können die Klassen zur Laufzeit geändert werden, sodass ein Testframework Methoden für vorhandene Klassen leicht stubben oder verspotten kann. Ruby ist eine solche Sprache; rspec ist das führende Test-Framework für TDD.
Wie dynamisches Schreiben das Testen unterstützt
Bei der dynamischen Typisierung können Sie Scheinobjekte erstellen, indem Sie einfach eine Klasse erstellen, die dieselbe Schnittstelle (Methodensignaturen) wie das zu verspottende Collaborator-Objekt hat. Angenommen, Sie hatten eine Klasse, die Nachrichten gesendet hat:
Angenommen, wir haben einen MessageSenderUser, der eine Instanz von MessageSender verwendet:
Beachten Sie hier die Verwendung der Abhängigkeitsinjektion , eine Grundvoraussetzung für Unit-Tests. Wir werden darauf zurückkommen.
Sie möchten testen, dass
MessageSenderUser#do_stuff
Anrufe zweimal gesendet werden. Genau wie in einer statisch typisierten Sprache können Sie einen nachgebildeten MessageSender erstellen, der zählt, wie oftsend
aufgerufen wurde. Im Gegensatz zu einer statisch typisierten Sprache benötigen Sie jedoch keine Schnittstellenklasse. Sie erstellen es einfach:Und verwenden Sie es in Ihrem Test:
An sich trägt die "Ententypisierung" einer dynamisch typisierten Sprache im Vergleich zu einer statisch typisierten Sprache nicht so viel zum Testen bei. Was aber, wenn Klassen nicht geschlossen sind, sondern zur Laufzeit geändert werden können? Das ist ein Game Changer. Mal sehen wie.
Was wäre, wenn Sie keine Abhängigkeitsinjektion verwenden müssten, um eine Klasse testbar zu machen?
Angenommen, MessageSenderUser verwendet MessageSender immer nur zum Senden von Nachrichten, und Sie müssen MessageSender nicht durch eine andere Klasse ersetzen. Innerhalb eines einzelnen Programms ist dies häufig der Fall. Lassen Sie uns MessageSenderUser so umschreiben, dass es einfach einen MessageSender ohne Abhängigkeitsinjektion erstellt und verwendet.
MessageSenderUser ist jetzt einfacher zu verwenden: Niemand, der es erstellt, muss einen MessageSender erstellen, damit es verwendet werden kann. In diesem einfachen Beispiel sieht es nicht nach einer großen Verbesserung aus, aber stellen Sie sich jetzt vor, dass MessageSenderUser an mehreren Stellen erstellt wird oder drei Abhängigkeiten aufweist. Jetzt hat das System eine ganze Reihe von Instanzen, nur um die Unit-Tests glücklich zu machen, nicht weil es das Design notwendigerweise überhaupt verbessert.
Mit offenen Klassen können Sie ohne Abhängigkeitsinjektion testen
Ein Testframework in einer Sprache mit dynamischer Typisierung und offenen Klassen kann TDD sehr schön machen. Hier ist ein Codefragment aus einem rspec-Test für MessageSenderUser:
Das ist der ganze Test. Wenn
MessageSenderUser#do_stuff
nichtMessageSender#send
genau zweimal aufgerufen wird, schlägt dieser Test fehl. Die echte MessageSender-Klasse wird niemals aufgerufen: Wir haben dem Test mitgeteilt, dass jemand, der versucht, einen MessageSender zu erstellen, stattdessen unseren nachgebildeten MessageSender erhalten sollte. Keine Abhängigkeitsinjektion erforderlich.Es ist schön, in einem so einfachen Test so viel zu tun. Es ist immer schöner, keine Abhängigkeitsinjektion verwenden zu müssen, es sei denn, dies ist für Ihr Design tatsächlich sinnvoll.
Aber was hat das mit offenen Klassen zu tun? Beachten Sie den Anruf an
MessageSender.should_receive
. Wir haben #should_receive nicht definiert, als wir MessageSender geschrieben haben. Wer hat das getan? Die Antwort ist, dass das Testframework, das einige sorgfältige Änderungen an Systemklassen vornimmt, es so aussehen lässt, als ob durch #should_receive für jedes Objekt definiert wird. Wenn Sie der Meinung sind, dass das Ändern solcher Systemklassen einige Vorsicht erfordert, haben Sie Recht. Aber es ist die perfekte Sache für das, was die Testbibliothek hier tut, und offene Klassen machen es möglich.quelle
'funktioniert gut mit TDD' reicht sicherlich nicht aus, um eine Sprache zu beschreiben, daher könnte sie wie alles "aussehen". Lisp, Prolog, C ++, Ruby, Python ... treffen Sie Ihre Wahl.
Darüber hinaus ist nicht klar, dass die Unterstützung von TDD am besten von der Sprache selbst gehandhabt wird. Sicher, Sie könnten eine Sprache erstellen, in der jeder Funktion oder Methode ein Test zugeordnet ist, und Sie könnten Unterstützung für das Erkennen und Ausführen dieser Tests einbauen. Unit-Testing-Frameworks behandeln den Erkennungs- und Ausführungsteil jedoch bereits gut, und es ist schwer zu erkennen, wie die Anforderung eines Tests für jede Funktion sauber hinzugefügt werden kann. Brauchen Tests auch Tests? Oder gibt es zwei Funktionsklassen - normale, die Tests benötigen, und Testfunktionen, die sie nicht benötigen? Das scheint nicht sehr elegant zu sein.
Vielleicht ist es besser, TDD mit Tools und Frameworks zu unterstützen. Bauen Sie es in die IDE ein. Erstellen Sie einen Entwicklungsprozess, der ihn fördert.
Wenn Sie eine Sprache entwerfen, ist es auch gut, langfristig zu denken. Denken Sie daran, dass TDD nur eine Methode ist und nicht jedermanns bevorzugte Arbeitsweise. Es mag schwer vorstellbar sein, aber es ist möglich, dass noch bessere Wege kommen. Möchten Sie als Sprachdesigner, dass die Leute Ihre Sprache verlassen müssen, wenn dies passiert?
Alles, was Sie wirklich sagen können, um die Frage zu beantworten, ist, dass eine solche Sprache dem Testen förderlich wäre. Ich weiß, dass das nicht viel hilft, aber ich denke, das Problem liegt in der Frage.
quelle
Dynamisch typisierte Sprachen erfordern keine expliziten Schnittstellen. Siehe Ruby oder PHP usw.
Auf der anderen Seite erzwingen statisch typisierte Sprachen wie Java und C # oder C ++ Typen und zwingen Sie, diese Schnittstellen zu schreiben.
Was ich nicht verstehe ist, was ist dein Problem mit ihnen. Schnittstellen sind ein Schlüsselelement des Designs und werden in allen Designmustern und unter Einhaltung der SOLID-Prinzipien verwendet. Ich verwende zum Beispiel häufig Schnittstellen in PHP, weil sie das Design explizit machen und auch das Design erzwingen. Auf der anderen Seite haben Sie in Ruby keine Möglichkeit, einen Typ zu erzwingen, es ist eine Sprache vom Typ Ente. Trotzdem müssen Sie sich die Benutzeroberfläche dort vorstellen und das Design in Ihrem Kopf abstrahieren, um es korrekt zu implementieren.
Obwohl Ihre Frage interessant klingt, bedeutet dies, dass Sie Probleme mit dem Verständnis oder der Anwendung von Abhängigkeitsinjektionstechniken haben.
Um Ihre Frage direkt zu beantworten, verfügen Ruby und PHP über eine hervorragende Verspottungsinfrastruktur, die sowohl in ihre Unit-Test-Frameworks integriert als auch separat bereitgestellt wird (siehe Mockery für PHP). In einigen Fällen können Sie mit diesen Frameworks sogar das tun, was Sie vorschlagen, z. B. statische Aufrufe oder Objektinitialisierungen verspotten, ohne explizit eine Abhängigkeit einzufügen.
quelle