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:
class MessageSender
def send
# Do something with a side effect
end
end
Angenommen, wir haben einen MessageSenderUser, der eine Instanz von MessageSender verwendet:
class MessageSenderUser
def initialize(message_sender)
@message_sender = message_sender
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
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 oft send
aufgerufen wurde. Im Gegensatz zu einer statisch typisierten Sprache benötigen Sie jedoch keine Schnittstellenklasse. Sie erstellen es einfach:
class MockMessageSender
attr_accessor :send_count
def initialize
@send_count = 0
end
def send
@send_count += 1
end
end
Und verwenden Sie es in Ihrem Test:
mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)
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.
class MessageSenderUser
def initialize
@message_sender = MessageSender.new
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
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:
mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff
Das ist der ganze Test. Wenn MessageSenderUser#do_stuff
nicht MessageSender#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.