Con los lenguajes más comunes (Java, C #, Java, etc.) a veces parece que está trabajando en desacuerdo con el idioma cuando desea TDD completo de su código.
Por ejemplo, en Java y C # querrás burlarte de cualquier dependencia de tus clases y la mayoría de los frameworks de burla te recomendarán burlarte de las interfaces, no de las clases. Esto a menudo significa que tiene muchas interfaces con una sola implementación (este efecto es aún más notable porque TDD lo obligará a escribir un mayor número de clases más pequeñas). Las soluciones que le permiten burlarse de clases concretas hacen cosas como alterar el compilador o anular los cargadores de clases, etc., lo cual es bastante desagradable.
Entonces, ¿cómo sería un lenguaje si estuviera diseñado desde cero para ser excelente para TDD? ¿Posiblemente de alguna manera a nivel de lenguaje para describir dependencias (en lugar de pasar interfaces a un constructor) y poder separar la interfaz de una clase sin hacerlo explícitamente?
Respuestas:
Hace muchos años, armé un prototipo que abordaba una pregunta similar; Aquí hay una captura de pantalla:
La idea era que las afirmaciones están en línea con el código en sí, y todas las pruebas se ejecutan básicamente en cada pulsación de tecla. Entonces, tan pronto como pase la prueba, verá que el método se vuelve verde.
fuente
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:
Digamos que tenemos un MessageSenderUser que usa una instancia de MessageSender:
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 vecessend
se llamó. Pero a diferencia de un lenguaje de tipo estático, no necesita una clase de interfaz. Simplemente sigue adelante y créalo:Y úsalo en tu prueba:
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.
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:
Esa es toda la prueba. Si
MessageSenderUser#do_stuff
no se invocaMessageSender#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.fuente
'funciona bien con TDD' seguramente no es suficiente para describir un lenguaje, por lo que podría "verse" como cualquier cosa. Lisp, Prolog, C ++, Ruby, Python ... elige tu opción.
Además, no está claro que admitir TDD es algo que se maneja mejor con el propio lenguaje. Claro, podría crear un lenguaje donde cada función o método tenga una prueba asociada, y podría construir un soporte para descubrir y ejecutar esas pruebas. Pero los marcos de pruebas unitarias ya manejan el descubrimiento y la ejecución de forma agradable, y es difícil ver cómo agregar limpiamente el requisito de una prueba para cada función. ¿Las pruebas también necesitan pruebas? ¿O hay dos clases de funciones: las normales que necesitan pruebas y las funciones de prueba que no las necesitan? Eso no parece muy elegante.
Quizás sea mejor admitir TDD con herramientas y marcos. Construirlo en el IDE. Crea un proceso de desarrollo que lo aliente.
Además, si está diseñando un lenguaje, es bueno pensar a largo plazo. Recuerde que TDD es solo una metodología, y no la forma preferida de trabajar de todos. Puede ser difícil de imaginar, pero es posible que surjan formas aún mejores . Como diseñador de idiomas, ¿quieres que la gente tenga que abandonar tu idioma cuando eso suceda?
Todo lo que realmente puede decir para responder a la pregunta es que tal lenguaje sería propicio para las pruebas. Sé que eso no ayuda mucho, pero creo que el problema está en la pregunta.
fuente
Bueno, los lenguajes escritos dinámicamente no requieren interfaces explícitas. Ver Ruby o PHP, etc.
Por otro lado, los lenguajes estáticamente tipados como Java y C # o C ++ imponen los tipos y lo obligan a escribir esas interfaces.
Lo que no entiendo es cuál es su problema con ellos. Las interfaces son un elemento clave del diseño y se utilizan en todos los patrones de diseño y en el respeto de los principios SÓLIDOS. Por ejemplo, con frecuencia uso interfaces en PHP porque hacen que el diseño sea explícito y también lo imponen. Por otro lado, en Ruby no tienes forma de imponer un tipo, es un lenguaje de tipo pato. Pero aún así, debe imaginar la interfaz allí y debe abstraer el diseño en su mente para implementarlo correctamente.
Entonces, aunque su pregunta puede sonar interesante, implica que tiene problemas para comprender o aplicar técnicas de inyección de dependencia.
Y para responder directamente a su pregunta, Ruby y PHP tienen una excelente infraestructura de burla, ambos integrados en sus marcos de prueba de unidad y entregados por separado (consulte Mockery para PHP). En algunos casos, estos marcos incluso le permiten hacer lo que está sugiriendo, como burlarse de llamadas estáticas o inicializaciones de objetos sin inyectar una dependencia explícitamente.
fuente