¿El TDD realmente conduce a un buen diseño?

26 Apr 2022 · Last updated: 12 may 15

Hace poco tuiteé que el TDD no puede conducir a un buen diseño si no sabemos qué aspecto tiene un buen diseño. También me refería a la necesidad de enseñar el diseño antes que el TDD (o al menos, al mismo tiempo). Este tuit condujo a una discusión con J.B. Rainsberger, Ron Jeffries y algunos otros. J.B. y yo terminamos teniendo una conversación en vivo en Hangout on Air más tarde.

Si revisas muchas de mis charlas, blogs e incluso mi libro, encontrarás múltiples ocasiones en las que digo que el TDD es una herramienta de diseño. Entonces, ¿qué ha cambiado? ¿Por qué ya no digo lo mismo?

¿Por qué he cambiado de opinión?

Después de prestar más atención a mi forma de trabajar y a la de muchos otros desarrolladores, me he dado cuenta de que no son muchos los que impulsan un buen diseño a través de TDD. Aunque me encanta el ritmo RED-GREEN-REFACTORING, tener un paso de "refactorización" no es suficiente para llamar al TDD una herramienta de diseño.

El TDD no prescribe cómo deberías diseñar. Lo que hace es preguntarte constantemente: "¿Estás seguro de esto?, ¿es suficientemente bueno?, ¿puedes mejorarlo?". Esta molestia (o recordatorio constante de mirar tu diseño y buscar posibles mejoras) es importante, pero no suficiente.

En mi opinión, el TDD es un flujo de trabajo de desarrollo de software que proporciona muchos beneficios, incluyendo el recordatorio constante de buscar mejoras en el código. Lo que significa hacer mi código mejor, no es parte de TDD.

¿Has olvidado las 4 Reglas del Diseño simple?

Ah, sí... Pero no. No me estoy olvidando de ellas. Las 4 Reglas de Diseño Simple NO son parte de TDD y aquí estoy hablando exclusivamente de TDD. 4Las 4 Reglas de Diseño Simple son normalmente las directrices de diseño que muchos expertos en TDD utilizan (incluyéndome a mí, entre otras técnicas) durante la fase de refactorización.

Las 4 Reglas del Diseño Simple es una de las muchas directrices de diseño que tenemos disponibles. SOLID y el Domain-Driven Design son otras. Más principios y patrones de diseño también están disponibles como buenas directrices. Estas son las cosas que necesitamos tener en nuestra mente durante la fase de "refactorización". O, poniéndolo de otra manera, tener una buena comprensión de las directrices de diseño existentes es lo que te llevará a un mejor diseño.

TDD es un flujo de trabajo (no una herramienta de diseño) en el que durante la fase de refactorización se aplican los conocimientos existentes sobre diseño de software combinados con técnicas de diseño que pueden ayudar a conseguir un diseño mejor.

No todos los TDD son iguales

Hay dos estilos principales de TDD con diferencias significativas entre ellos, principalmente en lo que respecta al diseño.

Clasicista

El enfoque clasicista es el enfoque original de TDD creado por Kent Beck. También se conoce como la Escuela de Detroit de TDD.

Características principales

  • El diseño tiene lugar durante la fase de refactorización.
  • Normalmente, las pruebas se basan en el estado.
  • Durante la fase de refactorización, la unidad sometida a prueba puede crecer hasta abarcar varias clases.
  • Los simulacros (o mocks) se utilizan poco, a no ser que se trate de aislar sistemas externos.
  • No se hacen consideraciones de diseño up-front. El diseño surge completamente del código.
  • Es una buena manera de evitar la sobreingeniería.
  • Es más fácil de entender y adoptar debido a las pruebas basadas en el estado y a la ausencia de diseño up-front.
  • A menudo se utiliza junto con las 4 Reglas del Diseño Simple.
  • Es bueno para la exploración, cuando sabemos cuál es la entrada y la salida que deseamos, pero no sabemos realmente qué aspecto tiene la implementación.
  • Ideal para casos en los que no podemos confiar en un experto en el dominio o en el lenguaje del dominio (transformación de datos, algoritmos, etc.)

Problemas

  • Exponer el estado sólo para pruebas.
  • La fase de refactorización es normalmente más grande en comparación con el enfoque Outside-In (más información a continuación).
  • La unidad bajo prueba se vuelve más grande que una clase cuando las clases emergen durante la fase de refactorización. Esto está bien cuando miramos esa prueba de forma aislada, pero a medida que las clases emergen, crean una vida propia, siendo reutilizadas por otras partes de la aplicación. A medida que estas otras clases evolucionan, pueden romper pruebas que no están relacionadas, ya que las pruebas utilizan su implementación real en lugar de un simulacro o mock.
  • El paso de refactorización (mejora del diseño) es a menudo omitido por los profesionales inexpertos, lo que lleva a un ciclo que se parece más a RED-GREEN-RED-GREEN-...-RED-GREEN-MASSIVE REFACTORING.
  • Debido a su naturaleza exploratoria, algunas clases en fase de prueba se crean según el "creo que necesitaré esta clase con esta interfaz (métodos públicos)", lo que hace que no encajen bien cuando se conectan con el resto del sistema.
  • Puede ser lento y un desperdicio, ya que a menudo sabemos que no podemos tener tantas responsabilidades en la clase bajo prueba. El consejo clasicista es esperar a la fase de refactorización para arreglar el diseño, basándose únicamente en pruebas concretas para extraer otras clases. Aunque esto es bueno para los novatos, es un puro desperdicio para los desarrolladores más experimentados.

Outside-In

Outside-In TDD, también conocido como London School o mockist, es un estilo de TDD desarrollado y adoptado por algunos de los primeros practicantes de XP en Londres. Posteriormente inspiró la creación de BDD.

Características principales

  • A diferencia del clasicista, Outside-In TDD prescribe una dirección en la que empezamos a probar nuestro código: desde el exterior (primera clase que recibe una petición externa) hasta el interior (clases que contendrán piezas individuales de comportamiento que satisfacen a la característica que se está implementando).
  • Normalmente empezamos con una prueba de aceptación que verifica si la característica en su conjunto funciona. La prueba de aceptación también sirve de guía para la implementación.
  • Con una prueba de aceptación fallida que informa de por qué la función aún no se ha completado (no se han devuelto datos, no se ha enviado ningún mensaje a una cola, no se han almacenado datos en una base de datos, etc.), empezamos a escribir pruebas unitarias. La primera clase que hay que probar es la que gestiona una petición externa (un controlador, un oyente de cola, un controlador de eventos, el punto de entrada de un componente, etc.)
  • Como ya sabemos que no vamos a construir toda la aplicación en una sola clase, hacemos algunas suposiciones sobre qué tipo de colaboradores necesitará la clase bajo prueba. A continuación, escribimos pruebas que verifican la colaboración entre la clase bajo prueba y sus colaboradores.
  • Los colaboradores se identifican en función de todo lo que debe hacer la clase objeto de la prueba cuando se invoca su método público. Los nombres de los colaboradores y los métodos deben proceder del lenguaje del dominio (sustantivos y verbos).
  • Una vez probada una clase, escogemos el primer colaborador (que fue creado sin implementación) y probamos su comportamiento, siguiendo el mismo enfoque que utilizamos para la clase anterior. Por eso lo llamamos outside-in: empezamos por las clases que están más cerca de la entrada del sistema (outside) y nos movemos hacia el interior de nuestra aplicación a medida que se identifican más colaboradores.
  • El diseño comienza en la fase roja, mientras se escriben las pruebas.
  • Las pruebas se refieren a la colaboración y al comportamiento, no al estado.
  • El diseño se perfecciona durante la fase de refactorización.
  • Cada colaborador y sus métodos públicos se crean siempre para servir a una clase cliente existente, haciendo que el código se lea muy bien.
  • Las fases de refactorización son mucho más reducidas, en comparación con el enfoque clásico.
  • Promueve una mejor encapsulación ya que no se expone ningún estado sólo para fines de prueba.
  • Más alineado con el enfoque de decir, no preguntar.
  • Más alineado con las ideas originales de la Programación Orientada a Objetos: las pruebas consisten en que los objetos envíen mensajes a otros objetos en lugar de comprobar su estado.
  • Adecuado para aplicaciones empresariales, donde los nombres y los verbos pueden extraerse de las historias de usuario y los criterios de aceptación.

Problemas

  • Mucho más difícil de adoptar para los principiantes, ya que es necesario un mayor nivel de conocimientos de diseño.
  • Los desarrolladores no obtienen información del código para crear colaboradores. Necesitan visualizar a los colaboradores mientras escriben la prueba.
  • Puede llevar a un exceso de ingeniería debido a la creación prematura de tipos (colaboradores).
  • No es adecuado para el trabajo exploratorio o comportamientos no especificados en una historia de usuario (transformación de datos, algoritmos, etc.).
  • Unas malas habilidades de diseño pueden llevar a una explosión de mocks.
  • Las pruebas de comportamiento son más difíciles de escribir que las de estado.
  • Se requiere el conocimiento del Domain Driven Design y otras técnicas de diseño, incluyendo las 4 Reglas del Diseño Simple, mientras se escriben las pruebas.

¿Qué estilo de TDD debemos utilizar?

Ambos. Todas. Son sólo herramientas y, como tales, deben usarse según las necesidades. Los practicantes experimentados de TDD saltan de un estilo a otro sin preocuparse nunca de qué estilo están utilizando.

Diseño macro y micro

Hay dos tipos de diseño: macro y micro diseño. El microdiseño es lo que hacemos mientras probamos el código, principalmente utilizando el enfoque clasicista. El macrodiseño va más allá de la función que estamos implementando. Se trata de cómo modelamos nuestro dominio a un nivel mucho más alto, cómo dividimos nuestra aplicación, capas, servicios, etc. El macrodiseño nos ayuda con la organización general de la aplicación y proporciona vías para que los equipos y los desarrolladores trabajen en paralelo sin pisarse unos a otros. El macrodiseño se refiere a la forma en que la empresa ve la aplicación y se suelen utilizar técnicas como el Domain-Driven Design. El macrodiseño también ayuda a la coherencia en toda la aplicación. TDD no le ayudará con el macrodiseño.

El macrodiseño se suele tener en cuenta cuando se utiliza el Outside-In TDD, pero el Outside-In por sí solo no es suficiente para definir el macrodiseño de una aplicación.

Conclusión

A lo largo de los años he visto muchas aplicaciones que han sido impulsadas por pruebas y seguían siendo un dolor de cabeza para trabajar. De acuerdo, admito que eran significativamente mejores que la mayoría de las aplicaciones legacy que no tenían pruebas que tenía que mantener antes.

Cualquier desarrollador puede hacer un desastre independientemente de si está escribiendo pruebas o no. Los desarrolladores también pueden hacer pruebas malas independientemente del estilo TDD que estén utilizando.

TDD no es una herramienta de diseño. Es un flujo de trabajo de desarrollo de software que tiene indicaciones para mejorar el código en su ciclo de vida. En estas indicaciones (escribir pruebas y refactorizar), los desarrolladores necesitan conocer algunas pautas de diseño (4 Reglas de Diseño Simple, Domain Driven Design, SOLID, Patrones, Ley de Demeter, Tell, Don't Ask, POLA/S, Diseño por Contrato, Feature Envy, cohesión, acoplamiento, Principio de Abstracción Equilibrada, etc) para mejorar su código. Sólo decir refactorización no es suficiente para llamar a TDD una herramienta de diseño.

Muchos desarrolladores culpan al TDD y a los mocks de ralentizarlos. Acaban abandonando TDD porque les cuesta conseguir el resultado que quieren. En mi opinión, ningún desarrollador lucha realmente por entender el ciclo de vida RED-GREEN-REFACTOR. Lo que les cuesta es diseñar bien el software.

Lo bueno de TDD es que nos pregunta constantemente "¿Puedes mejorar tu código? ¿Ves lo difícil que se está volviendo probar esta clase? Bien, lo has hecho funcionar. Aquí está tu barra verde. Ahora, hazlo mejor". Además de eso, estás por tu cuenta.

El TDD se hace mucho más fácil cuando entendemos cómo es un buen diseño. Practicar y comprender la gran cantidad de directrices de diseño disponibles hará que el TDD sea mucho más fácil y útil. También reducirá su curva de aprendizaje y, con suerte, aumentará su adopción.

Los extremos son malos. Estamos pasando del BDUF (Big Design Up Front) a no diseñar en absoluto. Tirar a la basura nuestros conocimientos de diseño es un error. Claro que no debemos volver a la época oscura y sobredimensionar todo, pero pensar que sólo debemos centrarnos en el microdiseño también es un error. Si estás trabajando por tu cuenta, haciendo unas pocas katas, o trabajando en una pequeña aplicación, entonces sí, haz lo que quieras. Pero si formas parte de un equipo más grande desarrollando algo que es significativamente mayor que una kata, le harás un favor a tu equipo si prestas más atención al macrodiseño y a cómo estructurar tu código.