Sería escrito dinámicamente en lugar de estáticamente. La escritura de pato haría el mismo trabajo que las interfaces en los idiomas de escritura estática. Además, sus clases serían modificables en tiempo de ejecución para que un marco de prueba pudiera fácilmente tropezar o burlarse de los métodos en las clases existentes. Ruby es uno de esos idiomas; rspec es su principal marco de prueba para TDD.
Cómo la escritura dinámica ayuda a las pruebas
Con la escritura dinámica, puede crear objetos simulados simplemente creando una clase que tenga la misma interfaz (firmas de método) que el objeto colaborador que necesita simular. Por ejemplo, suponga que tiene alguna clase que envía mensajes:
class MessageSender
def send
# Do something with a side effect
end
end
Digamos que tenemos un MessageSenderUser que usa una instancia de MessageSender:
class MessageSenderUser
def initialize(message_sender)
@message_sender = message_sender
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
Tenga en cuenta el uso aquí de la inyección de dependencia , un elemento básico de las pruebas unitarias. Volveremos a eso.
Desea probar que las MessageSenderUser#do_stuff
llamadas se envían dos veces. Del mismo modo que lo haría en un lenguaje estáticamente escrito, puede crear un MessageSender simulado que cuente cuántas veces send
se llamó. Pero a diferencia de un lenguaje de tipo estático, no necesita una clase de interfaz. Simplemente sigue adelante y créalo:
class MockMessageSender
attr_accessor :send_count
def initialize
@send_count = 0
end
def send
@send_count += 1
end
end
Y úsalo en tu prueba:
mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)
Por sí solo, el "tipeo de pato" de un lenguaje de tipo dinámico no agrega mucho a las pruebas en comparación con un lenguaje de tipo estático. Pero, ¿qué pasa si las clases no están cerradas, pero pueden modificarse en tiempo de ejecución? Eso es un cambio de juego. A ver cómo.
¿Qué pasaría si no tuviera que usar la inyección de dependencia para hacer que una clase sea comprobable?
Suponga que MessageSenderUser solo usará MessageSender para enviar mensajes, y no tiene necesidad de permitir la sustitución de MessageSender por otra clase. Dentro de un solo programa, este suele ser el caso. Reescribamos MessageSenderUser para que simplemente cree y use un MessageSender, sin inyección de dependencia.
class MessageSenderUser
def initialize
@message_sender = MessageSender.new
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
MessageSenderUser ahora es más fácil de usar: nadie que lo cree necesita crear un MessageSender para que lo use. No parece una gran mejora en este simple ejemplo, pero ahora imagine que MessageSenderUser se crea en más de una vez, o que tiene tres dependencias. Ahora el sistema tiene una gran cantidad de instancias que pasan solo para hacer felices las pruebas unitarias, no porque necesariamente mejore el diseño en absoluto.
Las clases abiertas te permiten probar sin inyección de dependencia
Un marco de prueba en un lenguaje con escritura dinámica y clases abiertas puede hacer que TDD sea bastante agradable. Aquí hay un fragmento de código de una prueba rspec para 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
Esa es toda la prueba. Si MessageSenderUser#do_stuff
no se invoca MessageSender#send
exactamente dos veces, esta prueba falla. La clase real de MessageSender nunca se invoca: le dijimos a la prueba que cada vez que alguien intenta crear un MessageSender, debería obtener nuestro falso MessageSender. No se necesita inyección de dependencia.
Es bueno hacer mucho en una prueba tan simple. Siempre es mejor no tener que usar la inyección de dependencia a menos que realmente tenga sentido para su diseño.
Pero, ¿qué tiene esto que ver con las clases abiertas? Tenga en cuenta la llamada a MessageSender.should_receive
. No definimos #should_receive cuando escribimos MessageSender, entonces, ¿quién lo hizo? La respuesta es que el marco de prueba, haciendo algunas modificaciones cuidadosas de las clases del sistema, puede hacer que aparezca como a través de #should_receive se define en cada objeto. Si crees que modificar clases de sistemas así requiere cierta precaución, tienes razón. Pero es lo perfecto para lo que la biblioteca de prueba está haciendo aquí, y las clases abiertas lo hacen posible.