Escribir código comprobable versus evitar la generalidad especulativa

11

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.

Erik Dietrich
fuente
Personalmente, creo que las entidades no deberían ser burladas. Solo me burlo de los servicios, y esos necesitan una interfaz en cualquier caso, porque el código de dominio de código generalmente no tiene referencia al código donde se implementan los servicios.
CodesInChaos
77
Nosotros, los usuarios de los idiomas escritos dinámicamente, nos reímos de que se burlen de las cadenas que le han puesto sus idiomas escritos estáticamente. Esto es una cosa que hace que las pruebas unitarias sean más fáciles en lenguajes escritos dinámicamente, no tengo que haber desarrollado una interfaz para sustituir un objeto con fines de prueba.
Winston Ewert
Las interfaces no solo se utilizan para generar generalidad. Se usan para muchos propósitos, desacoplando su código como uno de los más importantes. Lo que a su vez hace que la prueba de su código sea mucho más fácil.
Marjan Venema
@ WinstonEwert Ese es un beneficio interesante del tipeo dinámico que no había considerado antes como alguien que, como usted señala, no suele funcionar en lenguajes tipados dinámicamente.
Erik Dietrich
@CodeInChaos No había considerado la distinción a los fines de esta pregunta, aunque es una distinción razonable. En este caso, podríamos pensar en clases de servicio / marco con una sola implementación (actual). Digamos que tengo una base de datos a la que accedo con DAO. ¿No debería interactuar con los DAO hasta que tenga un modelo de persistencia secundario? (Eso parece ser lo que el autor de la publicación del blog está dando a entender)
Erik Dietrich

Respuestas:

14

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.

Daniel Mann
fuente
3
Absolutamente correcto. El código de prueba es parte de la aplicación porque lo necesita para hacer bien el diseño, la implementación, el mantenimiento, etc. El hecho de que no se lo envíe al cliente es irrelevante: si hay una segunda implementación en su conjunto de pruebas, entonces la generalidad está ahí y debe adaptarse.
Kilian Foth
1
Esta es la respuesta que me parece más convincente (y @KilianFoth agrega que si el código incluye una segunda implementación aún existe). Sin embargo, voy a esperar un poco para aceptar la respuesta para ver si alguien más interviene.
Erik Dietrich
También agregaría que, cuando dependa de las interfaces en las pruebas, la generalidad ya no es especulativa
Pete
"No hacerlo" (usando interfaces) no resulta automáticamente en que sus clases estén estrechamente acopladas. Simplemente no lo hace. Por ejemplo, en .NET Framework, hay una Streamclase, pero no hay un acoplamiento estrecho.
Luke Puplett
3

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.

S.Robins
fuente
1

¿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
Sí, trabajo en lenguaje (s) con marcos burlones que admiten burlarse de objetos concretos. Estas herramientas requieren diversos grados de saltar a través de aros para hacerlo.
Erik Dietrich
0

Contestaré en dos partes:

  1. 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.

  2. 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.

Gil Brandao
fuente
0

He visto muchas ventajas desde que comencé a programar más para una interfaz más allá del polimorfismo.

  • Me obliga a pensar más detenidamente sobre la interfaz de la clase (sus métodos públicos) y cómo interactuará con las interfaces de otras clases.
  • Me ayuda a escribir clases más pequeñas que son más coherentes y siguen al director de responsabilidad individual.
  • Es más fácil probar mi código
  • Menos clases estáticas / estado global ya que la clase debe ser de nivel de instancia
  • Es más fácil integrar y ensamblar piezas antes de que todo el programa esté listo
  • Inyección de dependencias, que separa la construcción de objetos de la lógica empresarial.

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.

Despertar
fuente