mobile menu icon

Heuristicas para determinar los límites de una unidad: estereotipos de pares, detección de efectos y propiedades FIRS

Publicado por Manuel Rivero el 26/06/2026

Testing, TDD, Test Doubles, Legacy Code, Object-Oriented Design


Introducción.

En nuestra publicación anterior, La clase no es la unidad en el estilo de TDD de Londres, hablamos sobre la distinción que los autores del libro GOOS hacen entre los pares (colaboradores reales[1]) y los objetos internos de un objeto, y cómo esta distinción era crucial para la mantenibilidad de los test unitarios.

Comentamos que, en los test que escribimos, solo deberíamos usar en dobles de prueba para simular los pares de un objeto porque se alinean con los comportamientos (roles, responsabilidades[2]) de los que depende directamente el comportamiento que estamos probando, y no con las clases de las que depende. Esta práctica es coherente con la recomendación: “mock roles, not objects”[3]. Esto nos ayuda a centrarnos en testear el comportamiento en lugar de los detalles de implementación.

También comentamos que los dobles de prueba no deberían simular objetos internos (cualquier colaborador que no sea un par del objeto que se está testeando), ya que estos representan detalles de implementación. Utilizar dobles de prueba para simularlos puede dar lugar a tests estrechamente acoplados a la estructura en lugar de al comportamiento.

Por lo tanto, para reducir el acoplamiento de nuestros tests a los detalles de implementación, es crucial que identifiquemos correctamente los pares de un objeto.

Por último, comentamos los estereotipos de pares, que son heurísticas presentadas por los autores del GOOS para ayudarnos a reflexionar sobre nuestro diseño e identificar los pares.

En esta publicación analizaremos diferentes heurísticas que podemos aplicar para detectar los pares de un objeto o, dicho de otro modo, para determinar los límites de la unidad que se está testeando. También examinaremos las relaciones entre ellas.[4].

Heurísticas para determinar los límites de la unidad que se está testeando.

Heurística 1: Estereotipos de pares.

Según el GOOS, los pares de un objeto son objetos cohesivos[5] que pueden clasificarse de manera aproximada en tres tipos de relación:

Estos estereotipos de pares deben considerarse como heurísticas que nos ayudan a reflexionar sobre nuestro diseño, no como reglas estrictas.

Nos ayudan a definir los límites de la unidad porque las dependencias, las notificaciones y los ajustes deberían estar fuera de la unidad.

A continuación, introduciremos dos heurísticas más generales que producen unidades de grano más grueso y explicaremos cómo estas heurísticas se relacionan con uno de los estereotipos de pares: las dependencias.

Heurística 2: Cumplimiento de FIRS.

Las propiedades FIRS (Fast, Isolated, Repeatable, Self-validating),[6] proporcionan una guía interesante para delimitar los límites de una unidad.

Según esta idea, cualquier código que cumpla las propiedades FIRS puede pertenecer a la unidad, mientras que cualquier código que viole alguna de estas propiedades constituye una colaboración incómoda (una dependencia que perjudica la testeabilidad) y debería ser expulsada fuera de la unidad.

Podemos sacar el código que viola FIRS (es decir, las colaboraciones incómodas) fuera de la unidad aplicando el Principio de Inversión de Dependencias (DIP). Esto nos permite controlar cómo la unidad depende de las dependencias incómodas y posibilita el uso de dobles de prueba en nuestros tests para simular el comportamiento de esas dependencias, evitando así los problemas de testeabilidad[7] (es decir, las violaciones de FIRS) que introducen.

Heurística 3: Detección de efectos.

Otra guía para determinar los límites de una unidad está inspirada en la separación que se hace en la programación funcional entre código con efectos (impuro)[8] y código sin efectos (puro). Desde esta perspectiva, los límites de la unidad emergen “allí donde es necesario realizar un efecto”.

Si no consideramos la mutación del estado como un efecto, los límites de la unidad definidos mediante el cumplimiento de FIRS y el aislamiento del código sin efectos se alinearán en gran medida. Esto no resulta sorprendente, ya que las violaciones de FIRS suelen estar causadas por efectos (excepto en el caso de cálculos puros realmente lentos).

¿Cómo se relacionan estas heurísticas?

Hasta ahora, hemos visto tres heurísticas que nos conducen a diseños que pueden ser testeados unitariamente.

Ya comentamos que los límites de la unidad definidos mediante el cumplimiento de FIRS y el aislamiento del código sin efectos se alinean en gran medida porque las violaciones de FIRS suelen estar causadas por efectos.

Además, creemos que los límites de la unidad, derivados de aplicar el cumplimiento de FIRS o de detectar efectos, se alinean estrechamente con los identificados al utilizar el estereotipo dependencias. Recordemos que este estereotipo se define como “servicios que un objeto necesita de su entorno para cumplir sus responsabilidades”.

Asimismo, estos límites de unidad se alinearían con los que produce el estilo clásico de TDD, ya que en él los dobles de prueba se utilizan principalmente como herramientas de aislamiento para evitar efectos o violaciones de FIRS en los tests.

Obsérvese que nos hemos centrado en el estereotipo dependencias como la heurística que define límites de unidad similares a los derivados de aplicar las heurísticas de cumplimiento de FIRS y detección de efectos. Más adelante exploraremos cómo los otros dos estereotipos de pares, ajustes y notificaciones, contribuyen a conseguir unidades de grano más fino y tests más mantenibles.

¿Cómo se relacionan estas heurísticas con el patrón Ports & Adapters?

Las heurísticas de cumplimiento de FIRS y detección de efectos delimitan fronteras donde nuestra aplicación es testeable independientemente de su contexto, lo que las convierte en un buen punto de partida para definir los puertos de nuestra aplicación. Sin embargo, donde se quedan cortas es en expresar las interfaces de los puertos en términos de la propia aplicación. Si utilizáramos únicamente estas dos heurísticas, podríamos acabar con interfaces de puertos que no estén alineadas con el lenguaje de dominio de la aplicación[9].

Por el contrario, utilizar el estereotipo dependencias proporciona una ventaja respecto a las dos heurísticas anteriores, ya que conduce a interfaces de puertos mejor diseñadas. Recordemos que este estereotipo se define como “servicios que un objeto necesita de su entorno para cumplir con sus responsabilidades”. Como resultado, las interfaces de puertos creadas utilizando este enfoque se alinean más estrechamente con el patrón ports & adapters, porque reflejarán el lenguaje y los conceptos definidos por la propia aplicación.

Además, también podemos aplicar los otros dos estereotipos de pares, ajustes y notificaciones, para diseñar unidades de grano más fino e independientes del contexto. Este enfoque puede dar lugar a límites de unidad que se alinearán con los que produciría una generalización del patrón ports & adapters (recordemos que ports & adapters solo aplica en las fronteras de la aplicación). Alistair Cockburn se ha referido recientemente a esta generalización como el patrón Component + Strategy.

¿Qué heurísticas solemos aplicar para determinar los límites de la unidad que se está testeando?

Todas ellas.

En el contexto de introducir tests en código legado, nos solemos centrar principalmente en detectar efectos y violaciones de FIRS para determinar dónde introducir costuras (seams), teniendo presente que las interfaces a las que se acoplaran los tests resultantes probablemente sean demasiado de bajo nivel y poco adecuadas para la aplicación, sufriendo del antipatrón Mimic Adapter[10].

Una vez hemos introducido tests usando esas costuras, refactorizaremos hacia interfaces más cohesivas y de mayor nivel de abstracción, utilizando los estereotipos de pares y el patrón Component + Strategy como guía para mejorar la modularidad y la claridad del diseño.

Cuando hacemos TDD, identificar los límites y las interfaces adecuados de la unidad puede ser más complicado porque debemos inferirlos a partir de los requisitos. En este caso, utilizamos las tres heurísticas para determinar los límites y las interfaces, y producir un diseño testeable.

Identificamos las colaboraciones incómodas detectando efectos o violaciones de FIRS en la especificación. Además, utilizamos los estereotipos de pares, especialmente el estereotipo dependencias, para definir las interfaces en términos de la unidad que estamos desarrollando mediante TDD.

¿Qué ocurre con los otros dos estereotipos de pares: “notificaciones” y “ajustes o políticas”?

Hasta ahora solo hemos hablado de utilizar el estereotipo dependencias para delimitar los límites de la unidad que se está testeando.

Existen otros dos estereotipos de pares, notificaciones y ajustes. ¿Qué ocurre con ellos?

En las siguientes secciones veremos cómo estos otros dos estereotipos de pares pueden seguir separando responsabilidades y mantener la cohesión, dando lugar a unidades de grano más fino y a un código y unos tests más mantenibles.

Estereotipo ajustes.

Cuando existen variantes para alguna parte del comportamiento de un objeto desde el principio, o cuando una parte del comportamiento de un objeto comienza a evolucionar a un ritmo diferente al resto, existen varias formas de adaptar el código para acomodar estas variaciones de comportamiento.

Algunas opciones disponibles son la paramétrica, la polimórfica y la composicional.

No todas las opciones tienen los mismos beneficios e inconvenientes. Creemos que la opción composicional suele ser la más adecuada en el código orientado a objetos.[11].

Si elegimos añadir estas variaciones mediante composición, primero necesitamos encapsularlas en una abstracción separada para mantener la cohesión del objeto. Esta nueva abstracción sería un ajuste. De esta forma, los ajustes pueden utilizarse para modificar o adaptar el comportamiento del objeto a las necesidades del sistema mediante el uso de la composición.

Para añadir variaciones mediante composición, aplicamos el principio de inversión de dependencias, para asegurar que el objeto dependa de la nueva abstracción en lugar de depender de una implementación concreta de esta. Después utilizamos la inyección de dependencias para decidir qué implementación concreta del ajuste queremos utilizar.

El código resultante del objeto quedará protegido frente a cambios en las implementaciones de los ajustes[12].

Además, separar los ajustes del objeto no solo da lugar a un mejor diseño, sino que también nos permite escribir tests más mantenibles.

Por un lado, podemos escribir tests centrados únicamente en el comportamiento principal del objeto utilizando dobles de prueba para simular los ajustes. Este enfoque garantiza que el objeto se pruebe independientemente de las implementaciones concretas de sus ajustes.

Después, podemos escribir otros tests que verifiquen el comportamiento de cada variante concreta del ajuste.

Esta manera de trabajar hace que los tests del objeto estén más enfocados y sean más mantenibles. Al desacoplarlos de ajustes específicos, nos aseguramos de que esos tests no se vean afectados por cambios en algún ajuste o por la incorporación de nuevas implementaciones de algún ajuste.

Estereotipo notificaciones.

A veces existen comportamientos secundarios asociados a los cambios de estado o a las acciones significativas de un objeto. Añadir estos comportamientos secundarios directamente al objeto viola el principio de responsabilidad única (SRP) e introduce acoplamiento temporal[13] entre el comportamiento principal del objeto y sus comportamientos secundarios asociados.

Para cumplir con el SRP, los comportamientos secundarios asociados deben encapsularse en colaboradores.

Sin embargo, de esa manera el objeto queda fuertemente acoplado a dichos colaboradores, y además no se elimina el acoplamiento temporal.

Este diseño fuertemente acoplado introduce dificultades significativas en el desarrollo y mantenimiento del código y sus tests. Cada vez que se añade un nuevo comportamiento secundario, el objeto y sus tests deben modificarse, aumentando el riesgo de errores y haciendo que la base de código sea más costosa de mantener.

Además, este tipo de diseño suele dar lugar a tests frágiles, que se rompen con frecuencia a medida que el sistema evoluciona. Esta fragilidad suele atribuirse a los dobles de prueba, en lugar de reconocer que el problema proviene de los defectos de diseño subyacentes[14]. En su lugar, deberíamos “escuchar a nuestros tests”[15] y mejorar dichos defectos de diseño utilizando notificaciones.

Las notificaciones actúan como un mecanismo de desacoplamiento, que evita el acoplamiento temporal entre el comportamiento del objeto y los comportamientos secundarios encapsulados en las notificaciones.

Mediante las notificaciones, el objeto simplemente señaliza a los pares interesados (si los hay) cada vez que cambia de estado o realiza una acción significativa.

Estas notificaciones son comandos de tipo “fire and forget”[16], es decir, el objeto ni sabe ni le importa qué pares puedan estar escuchando. Esto garantiza un bajo acoplamiento entre componentes, haciendo que el diseño sea mucho más flexible y adaptable al cambio.

Una vez se han introducido las notificaciones, deberíamos evitar testear el comportamiento del objeto junto con todos sus comportamientos secundarios asociados.

La razón es que incluir los comportamientos secundarios asociados en los tests del objeto requeriría crear dobles de prueba tanto para las dependencias y estrategias del objeto como para los las dependencias y estrategias de los colaboradores notificados, dando lugar a configuraciones de tests complejas y difíciles de mantener. Esta dificultad aumenta a medida que crece el número de comportamientos secundarios.

En su lugar, podemos simplificar nuestros tests y evitar la fragilidad descrita anteriormente aprovechando la abstracción que proporcionan las notificaciones.

En primer lugar, escribimos tests centrados exclusivamente en verificar que el comportamiento del objeto desencadena correctamente las notificaciones esperadas. En estos tests utilizaremos dobles de prueba[17] para simular las notificaciones.

A continuación, escribimos otros tests que confirmen que una notificación desencadena los comportamientos secundarios esperados en sus receptores. Estos tests están desacopladas del objeto que produce las notificaciones.

Esta manera de trabajar hace que los tests del objeto estén más enfocados y sean más mantenibles, ya que quedan desacoplados tanto de cambios en las notificaciones como de la incorporación de nuevos comportamientos secundarios asociados.

Resumen.

Hemos explorado tres heurísticas diferentes para definir los límites de una unidad que pueden aplicarse tanto al introducir tests en código legado como al practicar TDD: los estereotipos de pares del libro GOOS, el cumplimiento de FIRS y la detección de efectos.

Analizamos cómo estos tres enfoques diferentes conducen con frecuencia a límites de unidad similares y cómo pueden complementarse entre sí. Los límites identificados mediante el cumplimiento de FIRS o la detección de efectos suelen ser muy similares a los derivados del estereotipo dependencias.

También destacamos que la ventaja del estereotipo dependencias es su enfoque en aquello que “el objeto necesita”. Este enfoque conduce a interfaces expresadas en el lenguaje del cliente que las usa.

Además, explicamos cómo estas heurísticas pueden ayudar a definir los límites de una aplicación, estableciendo una conexión con el patrón Ports & Adapters. De nuevo, el énfasis del estereotipo dependencias en las necesidades explícitas del objeto da lugar a mejores interfaces, ayudando a evitar el antipatrón mimic adapter.

Por último, vemos cómo los estereotipos ajustes y notificaciones reducen el acoplamiento, mejoran la cohesión y dan lugar a tests más mantenibles y enfocados.

En futuras publicaciones hablaremos sobre otras técnicas para detectar pares basadas en la detección de test smells, el aprovechamiento del conocimiento del dominio o los patrones de diseño.

La serie sobre TDD, dobles de prueba y diseño orientado a objetos.

Esta publicación forma parte de una serie sobre TDD, dobles de prueba y diseño orientado a objetos:

  1. La clase no es la unidad en el estilo de TDD de Londres.

  2. ¡”Isolated” test significa algo muy diferente para distintas personas!.

  3. Heuristicas para determinar los límites de una unidad: estereotipos de pares, detección de efectos y propiedades FIRS.

  4. Breaking out to improve cohesion (peer detection techniques), (aún por traducir).

  5. Refactoring the tests after a “Breaking Out” (peer detection techniques), (aún por traducir).

  6. Bundling up to reduce coupling and complexity (peer detection techniques), (aún por traducir).

Agradecimientos.

Me gustaría dar las gracias a Fran Reyes, Emmanuel Valverde, Fran Iglesias, Marabesi Matheus, Manu Tordesillas y Alfredo Casado por darme su opinión sobre varios borradores de esta publicación.

Por último, también me gustaría dar las gracias a Petra Nesti por la fotografía.

Referencias.

Notas.

[1] Cualquier objeto que ayuda a otro objeto a cumplir sus responsabilidades se denomina colaborador. Parece que este término proviene de las Class-responsibility-collaboration cards, propuestas originalmente por Ward Cunningham y Kent Beck como herramienta de enseñanza en su artículo A Laboratory For Teaching Object-Oriented Thinking.

En dicho artículo escriben: “la última dimensión que utilizamos para caracterizar los diseños orientados a objetos son los colaboradores de un objeto. Denominamos colaboradores a aquellos objetos que enviarán o recibirán mensajes durante el cumplimiento de las responsabilidades”.

En nuestro artículo anterior, La clase no es la unidad en el estilo de TDD de Londres, explicamos que, según el libro GOOS, los colaboradores de un objeto pertenecen a una de dos categorías: pares (colaboradores reales) u objetos internos (detalles de implementación).

[2] El estilo orientado a objetos descrito en el libro GOOS está influenciado por el Responsibility-driven design de Rebecca Wirfs-Brock.

[3] Extraído de Mock roles, not objects, de Steve Freeman, Nat Pryce, Tim Mackinnon y Joe Walnes.

[4] Esta publicación surgió como respuesta a un comentario en la publicación The class is not the unit in the London school style of TDD.

[5] Siguiendo el principio de responsabilidad única, es decir, objetos que son cohesivos. Véase la sección Object Peer Stereotypes del capítulo 6, Object-Oriented Style, del libro GOOS.

[6] En nuestra publicación ¡”Isolated” test significa algo muy diferente para distintas personas!, explicamos qué significa para nosotros cada una de las propiedades FIRS.

También comentamos el origen y la historia del acrónimo FIRST, citando las fuentes originales.

Por último, explicamos cómo interpretamos isolated de manera diferente a los autores del acrónimo FIRST; nuestra definición está más alineada con la interpretación de Beck de isolated.

[7] Las violaciones de Isolated también pueden evitarse utilizando fixtures, aunque esto puede conducir a tests más lentos que violen Fast.

Con la llegada de tecnologías como Testcontainers, tests que tradicionalmente se clasificaban como tests de integración ahora pueden cumplir las propiedades FIRS. El uso de Testcontainers ayuda a mantener el aislamiento, garantizando que los tests se ejecuten de forma independiente y sean repetibles independientemente de la configuración local del desarrollador.

[8] El código impuro realiza efectos, pero ¿qué son los efectos? Intentemos explicarlo de manera informal.

Cualquier código que utilice un input que no venga en la lista de argumentos de un método o que no sea inyectado a través de su constructor, en el caso de los objetos, o que cambie algo de su contexto que no sea devolver un valor de retorno, se considera código con efectos, y esos inputs y outputs ocultos se consideran efectos.

La mayoría de la gente llama efectos (side-effects) a esos inputs y outputs ocultos, pero algunas personas utilizan el término side-effect únicamente para los outputs ocultos y el término side-causes para los inputs ocultos para resaltar su naturaleza distinta (como hace Kris Jenkins en su publicación What Is Functional Programming?).

Según esta distinción, un efecto sería algo que un programa cambia en su entorno y una causa sería algo que un programa requiere de su entorno.

El código impuro es mucho más difícil de testear y comprender que el código puro.

[9] Según Alistair Cockburn, las interfaces de los puertos deberían expresarse en el lenguaje de la aplicación:

“Every interaction between the app and the outside world happens at a port interface, using the interface language the app itself defines”

(en la página 12 de la edición preliminar de su libro Hexagonal Architecture Explained).

Las interfaces que surgen al buscar violaciones de FIRS o al detectar efectos corren el riesgo de presentar un nivel de abstracción demasiado bajo, pudiendo caer en el antipatrón mimic adapter. Los estereotipos de pares ayudan a mitigar ese riesgo.

[10] Dado que las técnicas de ruptura de dependencias conllevan cierto riesgo ya que se aplican sin tests, intentamos reducir dicho riesgo introduciendo capas muy finas que contienen la menor cantidad posible de código que nos permita conseguir aislamiento. Por ello, lo más probable es que tengan un nivel de abstracción inferior al que requiere nuestra aplicación y que no estén alineadas con el lenguaje de la aplicación. Serían mimic adapters, pero en el contexto de una rotura de dependencias esto no constituyen un antipatrón, ya que son justamente lo que necesitamos para reducir riesgos al romper la dependencia para poder introducir tests.

Sin embargo, si una vez ya hemos introducido los tests no refactorizamos estas interfaces de bajo nivel hacia mejores abstraciones, podemos enfrentarnos a problemas de acoplamiento excesivo en nuestros tests (véase nuestra publicación An example of wrong port design detection and refinement).

[11] En el capítulo Deriving Strategy Pattern de su libro Flexible, Reliable Software Using Patterns and Agile Development, Henrik Bærbak Christensen describe y analiza en profundidad cuatro opciones, que denomina propuestas, para adaptar un diseño orientado a objetos a este tipo de variaciones de comportamiento: source tree copy proposal, parametric proposal, polymorphic proposal y compositional proposal.

La compositional proposal tiene muchos beneficios interesantes y algunos inconvenientes. Merece la pena leer el análisis completo.

[12] Esto tiene la interesante propiedad de cambiar el comportamiento añadiendo nuevo código de producción en lugar de modificar el existente. Esta propiedad se caracteriza como Change by Addition (en el libro de Henrik Bærbak Christensen), Open-Closed Principle o Protected Variations.

Personalmente prefiero la última descripción.

[13] “Cuando dos acciones se agrupan en un mismo módulo simplemente porque ocurren al mismo tiempo”. Véase la entrada sobre Coupling en Wikipedia.

[14] Otro ejemplo de culpar a una herramienta o técnica en lugar de reflexionar sobre los problemas de nuestro diseño o sobre la manera en que utilizamos dicha herramienta o técnica.

[15] Interpretando las dificultades al testear como una señal de retroalimentación que indica que nuestro diseño podría necesitar mejoras.

Echa un vistazo a esta interesante serie de publicaciones sobre escuchar a los tests, de Steve Freeman. Es una versión preliminar del contenido que encontrarás en el capítulo 20, Listening to the tests, del GOOS.

De hecho, según Nat Pryce, los mocks fueron diseñados como una herramienta de retroalimentación para diseñar código orientado a objetos siguiendo el principio ‘Tell, Don’t Ask’. Puedes leer más sobre ello en esta conversación del grupo de Google Growing Object-Oriented Software.

La retroalimentación suele venir en forma de “dolor” 😅.

[16] Véase Command Query Separation.

[17] Dado que las notificaciones son comandos, podemos utilizar mocks, fakes o spies.

Volver a posts