Estaba leyendo algunas publicaciones de blog esta mañana, y me topé con esta :
Si la única clase que implementa la interfaz del Cliente es CustomerImpl, realmente no tiene polimorfismo y sustituibilidad porque no hay nada en la práctica para sustituir en tiempo de ejecución. Es falsa generalidad.
Eso tiene sentido para mí, ya que la implementación de una interfaz agrega complejidad y, si solo hay una implementación, uno podría argumentar que agrega complejidad innecesaria. Escribir código que es más abstracto de lo que debe ser a menudo se considera un olor a código llamado "generalidad especulativa" (también mencionado en la publicación).
Pero, si estoy siguiendo TDD, no puedo (fácilmente) crear dobles de prueba sin esa generalidad especulativa, ya sea en forma de implementación de interfaz u otra opción polimórfica, haciendo que la clase sea heredable y sus métodos virtuales.
Entonces, ¿cómo podemos conciliar esta compensación? ¿Vale la pena ser especulativamente general para facilitar las pruebas / TDD? Si usa dobles de prueba, ¿cuentan como implementaciones secundarias y, por lo tanto, la generalidad ya no es especulativa? ¿Debería considerar un marco de burla más pesado que permita burlarse de colaboradores concretos (por ejemplo, Moles versus Moq en el mundo C #)? O, ¿debería probar con las clases concretas y escribir lo que podría considerarse pruebas de "integración" hasta el momento en que su diseño naturalmente requiera polimorfismo?
Tengo curiosidad por leer las opiniones de otras personas sobre este asunto, gracias de antemano por sus opiniones.
fuente
Respuestas:
Fui y leí la publicación del blog, y estoy de acuerdo con mucho de lo que dijo el autor. Sin embargo, si está escribiendo su código usando interfaces para propósitos de prueba de unidad, diría que la implementación simulada de la interfaz es su segunda implementación. Yo diría que realmente no agrega mucha complejidad a su código, especialmente si la compensación de no hacerlo resulta en que sus clases están estrechamente acopladas y son difíciles de probar.
fuente
Stream
clase, pero no hay un acoplamiento estrecho.Probar el código en general no es fácil. Si así fuera, lo hubiéramos estado haciendo hace mucho tiempo, y no hubiéramos hecho tan poco en los últimos 10-15 años. Una de las mayores dificultades siempre ha sido determinar cómo probar el código que se ha escrito de manera coherente, bien factorizado y comprobable sin romper la encapsulación. El director de BDD sugiere que nos centremos casi por completo en el comportamiento, y de alguna manera parece sugerir que realmente no necesita preocuparse por los detalles internos en un grado tan grande, pero esto a menudo puede hacer que las cosas sean bastante difíciles de probar si hay una gran cantidad de métodos privados que hacen "cosas" de una manera muy oculta, ya que puede aumentar la complejidad general de su prueba para tratar todos los resultados posibles a un nivel más público.
La burla puede ayudar hasta cierto punto, pero de nuevo está bastante centrada externamente. La inyección de dependencia también puede funcionar bastante bien, de nuevo con simulacros o dobles de prueba, pero esto también puede requerir que exponga elementos, ya sea a través de una interfaz o directamente, que de lo contrario hubiera preferido permanecer oculto; esto es particularmente cierto si desea tener un buen nivel de seguridad paranoico sobre ciertas clases dentro de su sistema.
Para mí, el jurado aún no sabe si diseñar sus clases para que sean más fácilmente comprobables. Esto puede crear problemas si necesita proporcionar nuevas pruebas mientras mantiene el código heredado. Acepto que deberías poder probar absolutamente todo en un sistema, pero no me gusta la idea de exponer, incluso indirectamente, los elementos internos privados de una clase, solo para poder escribir una prueba para ellos.
Para mí, la solución siempre ha sido adoptar un enfoque bastante pragmático y combinar una serie de técnicas para adaptarse a cada situación específica. Utilizo muchos dobles de prueba heredados para exponer propiedades y comportamientos internos para mis pruebas. Me burlo de todo lo que se puede adjuntar a mis clases, y donde no comprometa la seguridad de mis clases, proporcionaré un medio para anular o inyectar comportamientos con el propósito de probar. Incluso consideraré proporcionar una interfaz más controlada por eventos si ayuda a mejorar la capacidad de probar el código
Cuando encuentro un código "no comprobable" , veo si puedo refactorizar para hacer las cosas más comprobables. Cuando tienes un montón de código privado oculto detrás de escena, a menudo encontrarás nuevas clases a la espera de ser divididas. Estas clases se pueden usar internamente, pero a menudo se pueden probar de forma independiente con comportamientos menos privados y, a menudo, con menos capas de acceso y complejidad. Sin embargo, una cosa que debo evitar es escribir el código de producción con el código de prueba incorporado. Puede ser tentador crear " lengüetas de prueba " que resulten en la inclusión de horrores como
if testing then ...
, lo que indica un problema de prueba que no está completamente deconstruido y resuelto de manera incompleta.Puede que le resulte útil leer el libro de Patrones de prueba xUnit de Gerard Meszaros , que cubre todo este tipo de cosas con mucho más detalle de lo que puedo analizar aquí. Probablemente no hago todo lo que él sugiere, pero ayuda a aclarar algunas de las situaciones de prueba más difíciles de manejar. Al final del día, desea poder satisfacer sus requisitos de prueba mientras sigue aplicando sus diseños preferidos, y ayuda a comprender mejor todas las opciones para poder decidir mejor dónde podría comprometer.
fuente
¿El lenguaje que usa tiene una manera de "burlarse" de un objeto para probarlo? Si es así, estas molestas interfaces pueden desaparecer.
En una nota diferente, puede haber razones para tener un SimpleInterface y un único ComplexThing que lo implemente. Podría haber partes de ComplexThing que no desee que estén accesibles para el usuario de SimpleInterface. No siempre se debe a un codificador OO-ish demasiado exuberante.
Me alejaré ahora y dejaré que todos salten sobre el hecho de que el código que hace esto les "huele mal".
fuente
Contestaré en dos partes:
No necesita interfaces si solo le interesan las pruebas. Utilizo marcos de imitación para ese propósito (en Java: Mockito o easymock). Creo que el código que diseñe no debe modificarse con fines de prueba. Escribir código comprobable es equivalente a escribir código modular, por lo que tiendo a escribir código modular (comprobable) y probar solo las interfaces públicas de código.
He estado trabajando en un gran proyecto Java y me estoy convenciendo profundamente de que usar interfaces de solo lectura (solo getters) es el camino a seguir (tenga en cuenta que soy un gran admirador de la inmutabilidad). La clase de implementación puede tener definidores, pero ese es un detalle de implementación que no debe exponerse a capas externas. Desde otra perspectiva, prefiero la composición sobre la herencia (modularidad, ¿recuerdas?), Por lo que las interfaces también ayudan aquí. Estoy dispuesto a pagar el costo de la generalidad especulativa que dispararme en el pie.
fuente
He visto muchas ventajas desde que comencé a programar más para una interfaz más allá del polimorfismo.
Muchas personas estarán de acuerdo en que más clases más pequeñas son mejores que menos clases más grandes. No tiene que concentrarse tanto en una sola vez y cada clase tiene un propósito bien definido. Otros pueden decir que está agregando a la complejidad al tener más clases.
Es bueno usar herramientas para mejorar la productividad, pero creo que confiar únicamente en los marcos Mock y en lugar de construir la capacidad de prueba y la modularidad directamente en el código dará como resultado un código de menor calidad a largo plazo.
En general, creo que me ha ayudado a escribir un código de mayor calidad y los beneficios superan con creces cualquier consecuencia.
fuente