7. Testabilidad y aislamiento del dominio

La arquitectura hexagonal fue diseñada para aislar las reglas de negocio de la infraestructura y, por lo tanto, facilitar las pruebas. Cuando el dominio no conversa directamente con la base de datos ni con la red, resulta sencillo validar el comportamiento en ejecución local, en pipelines automáticos y durante sesiones de TDD. En este tema exploramos las estrategias para testear el corazón de una aplicación, los tipos de dobles de prueba que podemos emplear y un ejemplo completo de prueba de caso de uso con dependencias inyectadas.

Comprender estos mecanismos permite detectar errores de negocio en segundos, mantener suites rápidas y fiables, y reducir el tiempo que transcurre entre un cambio y su despliegue productivo.

7.1 Probar el dominio sin necesidad de base de datos o red

El dominio se modela con objetos que encapsulan reglas e invariantes. Al no depender de APIs externas, se puede instanciar directamente en una prueba y ejecutar sus métodos sin configurar servidores o conexiones. En la práctica, basta con construir entidades y servicios de dominio y verificar sus resultados.

Para lograrlo:

  • Define el dominio con clases inmutables o con estado controlado, evitando anotaciones de frameworks.
  • Provee constructores y métodos fáciles de invocar que expresen el lenguaje ubicuo.
  • Evita dependencias estáticas hacia singletons o recursos compartidos que requieran inicialización externa.

Con estas prácticas, las pruebas se limitan a crear objetos del dominio y afirmar que los cálculos o validaciones se ejecutan correctamente.

7.2 Uso de mocks o fakes para probar adaptadores

Cuando un caso de uso requiere colaborar con puertos de salida, podemos simularlos mediante dobles de prueba. Los mocks permiten verificar interacciones, mientras que los fakes implementan una versión simple de la interfaz, como una lista en memoria. Herramientas como Mockito facilitan la creación de estas simulaciones, aunque también es posible escribirlas a mano para mantener claridad.

Al sustituir los adaptadores por dobles de prueba, el caso de uso se testea en aislamiento. El dominio valida su comportamiento sin requerir bases de datos, brokers de mensajería ni servicios externos reales. Luego se pueden agregar pruebas de integración específicas para cada adaptador, lo que reduce la complejidad de los escenarios.

7.3 Ejemplo de prueba de caso de uso con dependencias inyectadas

Supongamos un caso de uso que otorga bonos de fidelidad. El dominio define un puerto para persistir el bono y otro para publicar una notificación. La prueba a continuación utiliza JUnit 5 y un fake en memoria para validar el comportamiento.

package com.example.bonos.aplicacion;

import com.example.bonos.dominio.BonoFidelidad;
import com.example.bonos.dominio.PuertoBonos;
import com.example.bonos.dominio.PuertoEventos;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OtorgarBonoTest {

    private PuertoBonosFake puertoBonos;
    private PuertoEventosFake puertoEventos;
    private OtorgarBono casoDeUso;

    @BeforeEach
    void preparar() {
        puertoBonos = new PuertoBonosFake();
        puertoEventos = new PuertoEventosFake();
        casoDeUso = new OtorgarBono(puertoBonos, puertoEventos);
    }

    @Test
    void cuandoClienteCumpleCondiciones_seGuardaBonoYSePublicaEvento() {
        casoDeUso.ejecutar("CLIENTE-123", 120);

        assertEquals(1, puertoBonos.bonosGuardados.size());
        BonoFidelidad bono = puertoBonos.bonosGuardados.get(0);
        assertEquals("CLIENTE-123", bono.clienteId());
        assertTrue(puertoEventos.eventosPublicados.contains("BONO_OTORGADO"));
    }

    private static class PuertoBonosFake implements PuertoBonos {
        java.util.List<BonoFidelidad> bonosGuardados = new java.util.ArrayList<>();

        @Override
        public void guardar(BonoFidelidad bono) {
            bonosGuardados.add(bono);
        }
    }

    private static class PuertoEventosFake implements PuertoEventos {
        java.util.List<String> eventosPublicados = new java.util.ArrayList<>();

        @Override
        public void publicar(String codigoEvento, String payload) {
            eventosPublicados.add(codigoEvento);
        }
    }
}

El caso de uso OtorgarBono recibe los puertos como dependencias. La prueba los reemplaza por versiones fáciles de inspeccionar, manteniendo el aislamiento del dominio. Las aserciones verifican que la entidad se guardó y que se generó el evento correspondiente.

7.4 Ventajas para TDD y CI/CD

Esta forma de organizar el código beneficia directamente al desarrollo guiado por pruebas (TDD) y a las cadenas de integración continua:

  • Retroalimentación rápida: Las pruebas del dominio se ejecutan en milisegundos, lo que permite iterar con ciclos TDD cortos.
  • Confianza en los cambios: Al aislar cada caso de uso, cualquier regresión se detecta antes de desplegar. Las suites pueden ejecutarse en cada commit dentro de los pipelines de CI/CD.
  • Menor complejidad de entornos: No es necesario levantar bases de datos o servicios para validar la lógica de negocio, reduciendo el tiempo de preparación de los pipelines.
  • Documentación viva: Las pruebas describen el comportamiento esperado del dominio, facilitando la comprensión de nuevas funcionalidades y sirviendo como ejemplos ejecutables.

Al combinar la arquitectura hexagonal con un enfoque disciplinado de pruebas, se obtiene un ciclo de entrega fluido: el dominio se mantiene estable y expresivo, mientras que la infraestructura puede evolucionar con pruebas específicas que validan su integración.