Integrando TestContainers en el contexto de Spring en nuestros tests
Tags: testing, microservices, springboot, docker
Published: 2020-12-22 08:20:35

> _Aún me acuerdo de cómo hace muuuuchos años, en "la edad de los hierros", configurar un entorno local para desarrollar partes de plataformas complejas era un drama, y cómo se acababan configurando hosts in-house para replicar esa infraestructura externa y poder trabajar en equipo sobre la misma base._
>
> _Pero, a la hora de ejecutar tests en esas máquinas, a la que se necesitara algún cambio de versión mientras se seguía desarrollando sobre lo antiguo, o simplemente cambiar el tipo de dato de una columna de BBDD, drama otra vez, o replicarse esa máquina en local "temporalmente"._

En este post os daré una pincelada a cómo podemos integrar TestContainers en el flujo de ejecución de tests con [Junit5](https://junit.org/junit5/docs/current/user-guide/) para un servicio desarrollado con [Spring Boot](https://spring.io/projects/spring-boot), dejando que sea Spring quien haga el trabajo por nosotros, aprovechándonos de las características del ciclo de vida del ApplicationContext durante la ejecución de los tests.

Para ello, integraré `testcontainers` con distintas soluciones sobre un proyecto demo base, y os expondré corner cases en los que esas soluciones pueden no ser operativas, teniendo en cuenta que para los tests, y entornos de CI, por lo general es [una mala práctica fijar puertos.], de modo que este post se focalizará en la ejecución de tests con infraestructuras externas levantadas en local en puertos dinámicos.
(https://www.testcontainers.org/features/networking/) `From the host's perspective Testcontainers actually exposes this on a random free port. This is by design, to avoid port collisions that may arise with locally running software or in between parallel test runs.`

Para darle un poco de gracia al uso de contenedores tanto para el desarrollo local como para los tests, y entender por qué **usándolos podemos lograr reducir al máximo el riesgo de subir código a un entorno real cuando los tests pasan y "en local funciona"**, os propongo un servicio que no expondrá una API, sino que:
- debe escuchar mensajes de texto emitidos por otro servicio a una cola de RabbitMQ
- debe grabar esos mensajes a una base de datos PostgreSQL

A priori, un servicio muy sencillo, pero 
- cómo validaríamos que realmente funciona?
- cómo automatizaríamos esas validaciones?

> Como base, usaré este repositorio que implementa el servicio:
>
> :expressionless: _[Rama base, sin TestContainers](https://github.com/alextremp/testcontainers-springboot-demo), pero podremos probarlo haciendo hecho un `docker-compose up` previamente._

También, como referencia a las pruebas que comentaré por puntos al final del post, os dejo estas PR para poder ver cómo cambiaría el código base, o descargaros la rama para jugar.

> :confused: _[TestContainers integrados con JUnit](https://github.com/alextremp/testcontainers-springboot-demo/pull/3/files)_
> :rocket: _[TestContainers singleton gestionados manualmente](https://github.com/alextremp/testcontainers-springboot-demo/pull/2/files)_
> :heart_eyes: _[TestContainers gestionados por Spring](https://github.com/alextremp/testcontainers-springboot-demo/pull/1/files)_

Pero antes de empezar, una breve intro a [Docker Compose](https://docs.docker.com/compose/) y [TestContainers](https://www.testcontainers.org/)

# Docker Compose
Hoy en día, [Docker](https://www.docker.com/get-started) facilita y estandariza la forma en cómo los servicios de nuestras plataformas se despliegan en la nube, y [Docker Compose](https://docs.docker.com/compose/) nos facilita la vida en el entorno local, habilitándonos esos servicios de infraestructuras (bases de datos, sistemas de mensajería, ...) externas de un modo fiable.

Seguramente habréis visto en la raíz de gran cantidad de repositorios un fichero `docker-compose.yml`, por lo general muy fáciles de entender a primera vista, como por ejemplo:

```yaml
version: '3.5'

services:

  # base de datos PostgreSQL
  message-store-db:
    # https://hub.docker.com/_/postgres
    image: postgres:13.0-alpine
    # mapeo de puertos (puerto_accesible_desde_localhost:puerto_del_servicio_en_el_contenedor)
    ports:
      - "5432:5432"
    # configuración de la imagen de docker
    environment:
      POSTGRES_USER: "demo"
      POSTGRES_PASSWORD: "p4ssw0rd"
      POSTGRES_DB: "messagestore"
    # inicialización de la base de datos 
    volumes:
      - ./.init/message-store-db/init.sql:/docker-entrypoint-initdb.d/init.sql
```

Con instrucciones para levantar el repositorio en local, y que con un simple comando:

```bash
docker-compose up
```

Tenemos la infraestructura externa, en este caso una base de datos, lista en nuestra máquina para poder operar con ella desde nuestro servicio levantado en local, o incluso en este ejemplo, conectándonos directamente a PostgreSQL con algún visor de bases de datos.

> _Esto nos permite validar que nuestro proyecto, que usará un driver específico para la base de datos, ejecutando sentencias o configurando opciones que quizá no están disponibles para todas las versiones de ese sistema de base de datos, funciona._
>
> _Y como podremos levantar en local la misma versión de base de datos que tengamos en entorno real, el riesgo de que no funcione ahí se reduce a mínimos._
>
> _Esto no pasaría implementando los accesos a base de datos de los tests con por ejemplo, una base de datos en memoria como [H2](https://www.h2database.com/html/main.html), que por defecto no soporta PL/SQL._

Pero, si desarrollamos en local con esos servicios, mirando que nuestro servicio opera correctamente con ellos, **¿por qué no automatizar los tests de integración usando el mismo sistema y así poder ejecutar los tests tanto en nuestra máquina como en nuestro sistema de CI?**

# TestContainers
[TestContainers](https://www.testcontainers.org/) es una librería Java diseñada para controlar las instancias de servicios dockerizados, enfocada a ser integrada con JUnit/Spock/... para nuestros tests de integración.

Como ejemplo de uso, la misma PostgreSQL creada con la API de TestContainers quedaría así:

```java
PostgreSQLContainer messageStoreDbContainer =
      new PostgreSQLContainer<>("postgres:13.0-alpine")
        .withDatabaseName("message-store-db")
        .withUsername("demo")
        .withPassword("p4ssw0rd")
        .withInitScript("/.init/message-store-db/init.sql");
messageStoreDbContainer.setPortBindings(Arrays.asList("5432:5432"));

// podemos arrancar el servicio con
messageStoreDbContainer.start();

// y pararlo con
messageStoreDbContainer.stop();
```

Otro componente que nos ofrece, para permitirnos simular los tests con exactamente los mismos servicios que levantaríamos con el `docker-compose.yml`, es [DockerComposeContainer](https://www.testcontainers.org/modules/docker_compose/)

Un ejemplo de uso:

```java
WaitStrategy postgresWaitStrategy = new WaitAllStrategy(WITH_INDIVIDUAL_TIMEOUTS_ONLY)
  .withStrategy(Wait.forListeningPort())
  .withStrategy(Wait.forLogMessage(".*database system is ready to accept connections.*", 1));

DockerComposeContainer dockerComposeContainer =
  new DockerComposeContainer(new File("docker-compose.yml"))
    .withLocalCompose(true)
    .waitingFor("message-store-db", postgresWaitStrategy);

dockerComposeContainer.start();
```


> _Veréis que aquí se usa una funcionaliad opcional de la API de DockerComposeContainer para poder definir cómo debe esperarse el componente para decir que está ready hasta que el servicio "message-store-db" del "docker-compose.yml" esté ready._
>
> _Por defecto, se esperaría 60 segundos a que el servicio tuviera el puerto expuesto y listo para aceptar conexiones._
>
> _Ahora le estamos indicando que a parte de esperar al puerto, se espere también a que PostgreSQL, en su proceso de inicialización, haya sacado esa traza de log indicando que está completamente ready_


Para el caso propuesto, dado que los tests deben validar la integración con los servicios definidos en el `docker-composer`, usaré `DockerComposeContainer`.

> _Y en realidad, para la mayoría de casos es el único que vamos a necesitar._

Así que al lío :)

# ¿Cómo lo integramos en nuestros tests?

Nos tenemos que preguntar: ¿Los servicios de infraestructura externa deben cambiar para distintos escenarios de test, independientemente del contexto de Spring?

O por lo contrario, ¿Los servicios de infraestructura externa deben tener el mismo ciclo de vida que el contexto de Spring?

> Para ejemplificar las pruebas de integración de TestContainers en JUnit y SpringBoot, encontraréis este test sencillo en la rama _[master](https://github.com/alextremp/testcontainers-springboot-demo)_ del proyecto demo.

```java
@SpringBootTest
@Sql("/fixtures/message-store-db/clean.sql")
class ApplicationTest {

  @Autowired
  private RabbitTemplate rabbitTemplate;

  @Autowired
  private MessageMapper messageMapper;

  @Value("${mq-server.routing.exchange}")
  private String demoExchange;

  @Value("${mq-server.routing.message.queue.dispatched}")
  private String messageDispatchedQueue;

  @Test
  void shouldSaveEmittedMessage() {
    String givenText = "hello world";
    rabbitTemplate.convertAndSend(demoExchange, messageDispatchedQueue, givenText);

    await().atMost(Duration.ONE_SECOND)
          .alias(format("message [%s] has been saved", givenText))
          .until(() -> messageMapper.selectOneByText(givenText).isPresent());
  }

}
```

> Nota: para los que no la conozcáis, `await` es un operador de [awaitility](https://github.com/awaitility/awaitility), muy útil para validación de resultados en procesos asíncronos.

## Ciclo de vida controlado por JUnit

Empecemos con la chicha :)

  > _[código de referencia de este punto](https://github.com/alextremp/testcontainers-springboot-demo/pull/3/files)_

Si miramos la [documentación para integrar TestContainers con JUnit](https://www.testcontainers.org/test_framework_integration/junit_5), la integración básica es muy sencilla: mediante anotaciones de la propia librería, se habilita la gestión de arranque y parada de los servicios dockerizados, y éstos pueden exponer los puertos que ha podido coger por estar libres.

:warning: Pero ojo, nosotros necesitamos ciertas propiedades en el contexto de Spring, o dicho de otra manera, ¿cómo asignaremos los puertos designados a nuestras conexiones de base de datos y MQ?

En este caso, usaremos la capacidad de Spring de recoger propiedades de `System`, pudiendo configurar nuestros tests así:

```java
@SpringBootTest
@Testcontainers
abstract class AbstractIntegrationTest {

  private static final String MESSAGE_STORE_DB_SERVICE = "message-store-db";
  private static final Integer MESSAGE_STORE_DB_PORT = 5432;
  private static final String SPRING_MESSAGE_STORE_DB_PORT = "message-store-db.port";

  private static final String MQ_SERVER_SERVICE = "mq-server";
  private static final Integer MQ_SERVER_PORT = 5672;
  private static final String SPRING_MQ_SERVER_PORT = "mq-server.port";


  @Container
  private static DockerComposeContainer dockerServices =
        new DockerComposeContainer<>(new File("docker-compose.yml"))
              .withLocalCompose(true)
              .withExposedService(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)
              .withExposedService(MQ_SERVER_SERVICE, MQ_SERVER_PORT);

  @BeforeAll
  public static void beforeAll() {
    setProperty(SPRING_MESSAGE_STORE_DB_PORT, valueOf(dockerServices.getServicePort(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)));
    setProperty(SPRING_MQ_SERVER_PORT, valueOf(dockerServices.getServicePort(MQ_SERVER_SERVICE, MQ_SERVER_PORT)));
  }
}
```

En este caso, sólo que nuestro `ApplicationTest` extienda esta abstracción, al ejecutarse, dispondrá de todo lo necesario para funcionar porque:
- Antes de la ejecución de los tests, TestContainers mediante los eventos de JUnit habrá levantado el DockerComposeContainer.
- Después, JUnit procesará el `@BeforeAll`, donde hemos seteado las propiedades necesarias para el contexto de Spring, como `message-store-db.port`, con el valor que dispondrá cada servicio ya esperando conexiones.

Y el post terminaría aquí, si no fuera porque: 
**¿Funcionaría este approach si `AbstractIntegrationTest` fuera extendida por más de una implementación?** 
Correcto, en lugar de usar el ejemplo de TestContainers lo he puesto en una abstracción, porque como deduciréis, la respuesta es NO.

Si ejecutáramos sólo 1 clase de tests extendiendo `AbstractIntegrationTest`, ya veríamos una cosa que nos debería chirriar en los logs:

![docker-compose-down](https://dev-to-uploads.s3.amazonaws.com/i/ovr3qhp3v1txkna64ogb.png)

El test pasa, y al terminar, TestContainers intercepta el final de ejecución de los tests de la clase mediante JUnit y detiene el contenedor, antes de que se cierre el contexto de Spring.

**¿Por qué y por qué puede ser un problema?**

Eso pasa porque Spring, va a mantener el mismo `ApplicationContext` para la ejecución de todos los tests de todas las clases que no estén marcadas con `@DirtiesContext`, para evitar el coste de levantar el contexto para cada clase anotada con `@SpringBootTest`.

Y eso puede ser un problema si nos interesa hacer grupos de tests (por ejemplo, una clase de test para cada `@RestController` si estamos desarrollando APIs), ya que 
- los servicios dockerizados, al volverse a levantar, van a tener otro puerto libre designado para conectarnos a ellos.
- aunque se ejecute el `@BeforeAll` asignando nuevos puertos a `System`, el contexto de Spring no los va a usar dado que no se va a refrescar.

Y como seguramente no nos va a interesar que los servicios dockerizados se apaguen y levanten de nuevo para cada conjunto de tests, por el coste en tiempo, esto nos lleva a la gestión manual de los contenedores, ya que `There is no special support for this use case provided by the Testcontainers extension` tal como indica la sección [Singleton containers](https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/) de la documentación.

Pero no preocuparse, trabajamos con Spring por lo que los singletons no nos dan miedo, ¿no? ;)

## TestContainers Singleton gestionados manualmente, con ciclo de vida controlado por JUnit

  > _[código de referencia de este punto](https://github.com/alextremp/testcontainers-springboot-demo/pull/2/files)_

JUnit5 tiene una característica que nos permite anidar en una única clase, mediante `@Nested`, conjuntos de clases (test cases) que a su vez definen los tests a ejecutar.

Así que podemos:
- Tener una clase única, anotada con `@SpringBootTest` para levantar el contexto de Spring, en la que el ciclo de de vida de JUnit (`@BeforeAll` -> ejecución de los tests de cada test case -> `@AfterAll`) va a ser el mismo que el de Spring.
- Definir test cases independientes con su grupo de `@Test` a ejecutar.
- Anidar a la clase única estos test cases con `@Nested`

Algo tal que así:

```java
public class IntegrationTest {

  private static final DockerizedInfrastructure DOCKERIZED_INFRASTRUCTURE = new DockerizedInfrastructure();

  @BeforeAll
  static void dockerComposeUp() {
    DOCKERIZED_INFRASTRUCTURE.start();
  }

  @AfterAll
  static void dockerComposeDown() {
    DOCKERIZED_INFRASTRUCTURE.stop();
  }

  @Nested
  class Application extends ApplicationTestCase {
  }
}
```

Sólo necesitaremos que en este caso, `ApplicationTest` de la rama master, sea abstracta (y para este ejemplo, renombrada a `ApplicationTestCase` que define mejor su intención).

A parte, para hacer legible el ejemplo y evitar una mezcla de responsabilidades entre la coordinación de los tests que hará `IntegrationTest` y la coordinación de los servicios dockerizados, aquí sólo queda el singleton `DOCKERIZED_INFRASTRUCTURE` para poderlo gestionar al inicio y final de la ejecución de test cases, pero su implementación se ha separado a:

`DockerizedInfrastructure.java`
```java
public class DockerizedInfrastructure {

  private static final String MESSAGE_STORE_DB_SERVICE = "message-store-db";
  private static final Integer MESSAGE_STORE_DB_PORT = 5432;
  private static final String SPRING_MESSAGE_STORE_DB_PORT = "message-store-db.port";

  private static final String MQ_SERVER_SERVICE = "mq-server";
  private static final Integer MQ_SERVER_PORT = 5672;
  private static final String SPRING_MQ_SERVER_PORT = "mq-server.port";

  private final DockerComposeContainer dockerServices;

  public DockerizedInfrastructure() {
    // Inicializamos los servicios de docker-compose
    dockerServices = new DockerComposeContainer<>(new File("docker-compose.yml"))
          .withLocalCompose(true)
          .withExposedService(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)
          .withExposedService(MQ_SERVER_SERVICE, MQ_SERVER_PORT);
  }

  public void start() {
    // Arrancamos los servicios dockerizados
    dockerServices.start();

    // Seteamos System properties para propiedades del contexto de Spring
    setProperty(SPRING_MESSAGE_STORE_DB_PORT, valueOf(dockerServices.getServicePort(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)));
    setProperty(SPRING_MQ_SERVER_PORT, valueOf(dockerServices.getServicePort(MQ_SERVER_SERVICE, MQ_SERVER_PORT)));
  }

  public void stop() {
    // Apagamos los servicios dockerizados
    dockerServices.stop();
  }
}
```

Y llegados a este punto, que ahora sí funcionará tal como esperamos a medida que el proyecto vaya creciendo y necesitemos agrupar los tests en distintos test cases, otra vez daría el post por cerrado, si no fuera que **¿realmente es necesario que gestionemos manualmente un singleton y su ciclo de vida con JUnit para coordinarlo con el ciclo de vida del ApplicationContext de Spring?**

Y aunque no suponga un problema hacerlo, otra vez la respuesta es NO, dado que Spring, como ya comenté, va a mantener el `ApplicationContext` durante la ejecución de los tests y dispone de una serie de características que nos permitirán jugar con la gestión de los servicios dockerizados, las propiedades para el ApplicationContext (en este caso los puertos de los servicios),...

Además, con esta gestión manual, nos podríamos encontrar con los siguientes inconvenientes:

1. Añadir un nuevo test case requiere modificación en la base `IntegrationTest`. 

2. Ya sea por reagrupación de test cases o para configuraciones distintas por grupos de test cases, no podríamos tener 2 `IntegrationTest` distintos anidando distintos test cases, sin más. Por ejemplo:

Supongamos que replicamos el `ApplicationTestCase` varias veces y los anidamos en también una réplica de `IntegrationTest`:

![breaking-in-junit](https://dev-to-uploads.s3.amazonaws.com/i/9oo634dypw7g3js1d8ix.png)
 
Al ser réplicas, en los `IntegrationTestN`, `@BeforeAll` y `@AfterAll` reinician los contenedores pero de nuevo, nos encontramos que el `ApplicationContext` para los `ApplicationTestCaseN...` es el mismo y por tanto, sólo iniciará la conexión a las infraestructuras la primera vez que además, JUnit no va a ejecutar en un mismo orden, por lo que el segundo bloque de tests ejecutado va a fallar:

![tests-failed](https://dev-to-uploads.s3.amazonaws.com/i/ylxu1zdh5vwo5pvxtutt.png)

Para resolver este problema, tendríamos que limpiar el contexto de Spring para cada `ApplicationTestN`, de manera que cada test case creara un nuevo `ApplicationContext` con configuración fresca de los puertos activados, marcándolos todos ellos con dirties context:

```java
@SpringBootTest
@DirtiesContext
``` 

Esto de nuevo, aunque lógicamente es un corner case forzado para validar posibles flaquezas de la solución, nos lleva a pensar que realmente, lo ideal sería que la gestión del ciclo de vida de los servicios dockerizados fueran gestionados directamente con Spring.

Así, que como guindilla del pastel, continuamos :)

## TestContainers sólo gestionados por Spring

  > _[código de referencia de este punto](https://github.com/alextremp/testcontainers-springboot-demo/pull/1/files)_

Llegados a este punto, sabemos que:
- Los test de integración marcados con `@SpringBootTest` comparten el `ApplicationContext` a no ser que sean marcados con `@DirtiesContext`
- `DockerizedInfrastructure` debería ser tratado como un singleton (`@Component`) dentro del contexto de Spring para que en todo momento, el `ApplicationContext` pueda accederlo o regenerarlo en dirties.
- `DockerizedInfrastructure` debe reconfigurar las propiedades de conexión de la infraestructura.

:warning: pero, si hacemos un `@Component`, que Spring inicializa, ya no podemos setear las propiedades en `System`, dado que la lectura de beans (y propiedades) es anterior a la instanciación.

:bulb: pero por otro lado es Spring, y siempre tiene una solución a cómo gestionar estos casos: podemos usar el [`ConfigurableApplicationContext`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ConfigurableApplicationContext.html) para refrescar el contexto con las propiedades iniciales tal como nos va a interesar.

En definitiva, para que sea gestionado completamente por Spring, podríamos modificar `DockerizedInfrastructure.java` así:

```java
@Component
public class DockerizedInfrastructure {

  private static final String MESSAGE_STORE_DB_SERVICE = "message-store-db";
  private static final Integer MESSAGE_STORE_DB_PORT = 5432;
  private static final String SPRING_MESSAGE_STORE_DB_PORT = "message-store-db.port";

  private static final String MQ_SERVER_SERVICE = "mq-server";
  private static final Integer MQ_SERVER_PORT = 5672;
  private static final String SPRING_MQ_SERVER_PORT = "mq-server.port";

  private final DockerComposeContainer dockerServices;

  DockerizedInfrastructure(ConfigurableApplicationContext configurableApplicationContext) {
    // Inicializamos los servicios de docker-compose
    dockerServices = new DockerComposeContainer<>(new File("docker-compose.yml"))
          .withLocalCompose(true)
          .withExposedService(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)
          .withExposedService(MQ_SERVER_SERVICE, MQ_SERVER_PORT);

    // Arrancamos los servicios dockerizados
    dockerServices.start();

    // Refrescamos el contexto de Spring con los puertos designados
    TestPropertyValues.of(
          format("%s=%s",
                SPRING_MESSAGE_STORE_DB_PORT,
                dockerServices.getServicePort(MESSAGE_STORE_DB_SERVICE, MESSAGE_STORE_DB_PORT)),
          format("%s=%s",
                SPRING_MQ_SERVER_PORT,
                dockerServices.getServicePort(MQ_SERVER_SERVICE, MQ_SERVER_PORT))
    ).applyTo(configurableApplicationContext.getEnvironment());
  }

  @PreDestroy
  void preDestroy() {
    // Apagamos los servicios dockerizados
    dockerServices.stop();
  }
}
```

Fijaros que aquí, `DockerizedInfrastructure` no tiene ni el constructor ni ningún método público, dado que ahora no es necesario que ninguna clase de test acceda a ella para manipular su estado. _Sí lo es la declaración de la clase, luego veremos por qué :)_

Con el `@PreDestroy` nos aseguramos de apagar los servicios dockerizados cuando el `ApplicationContext` emita la señal de que está siendo destruido, que será o cuando hayan terminado los tests, o cuando un test esté marcado con `@DirtiesContext`, antes de la recreación del contexto.

Ésta sería una manera muy elegante de integrar esta `DockerizedInfrastructure` dado que no haría falta modificar nada en nuestra configuración de los tests, pero **¿qué pasaría si algún componente del servicio necesitara acceder a la base de datos durante la creación del contexto de Spring?**

Para forzar el caso, os pongo este ejemplo:

```java
@Component
public class InventBreaker {

  private final MessageMapper messageMapper;

  public InventBreaker(MessageMapper messageMapper) {
    this.messageMapper = messageMapper;
  }

  @PostConstruct
  void tryToBreakTheTestContext() {
    messageMapper.selectOneByText("applicationContext lanzará excepción si no hay conectividad a la BBDD");
  }
}
```

Supongamos que `InventBreaker`, por lo que sea, necesita inicializarse con un valor de la BBDD, y si no hacemos nada, va a pasar esto:

![breaking-the-invent](https://dev-to-uploads.s3.amazonaws.com/i/zan9kmieugyve8ftkxwq.png)
 
Spring, la única instrucción a la que va a hacer caso para que `InventBreaker` se espere a que `DockerizedInfrastructure` haya hecho la magia es con `@DependsOn("dockerizedInfrastructure")`, **pero wait!** Si `InventBreaker` forma parte del código del servicio en lugar del código de test, no podemos hacer esto.

:bulb: pero de nuevo, Spring ya contempla casi todos los corner cases que nos podamos imaginar, y nos va a permitir sincronizar la creación del contexto requerido para test con el contexto requerido para la aplicación modificando sólo una línea en la declaración de `@SpringBootTest`, dejando nuestra abstracción de tests de integración así:

```java
@SpringBootTest(classes = {DockerizedInfrastructure.class, Application.class})
```

Y listo! Ahora todo se inicializará y se apagará según el orden esperado.

Además, podríamos abstraer esta definición para ser usada en cualquier otra clase de test así:

`AbstractIntegrationTest.java`
```java
@SpringBootTest(classes = {DockerizedInfrastructure.class, Application.class})
abstract class AbstractIntegrationTest {
}
```

Y todas las clases de test con un `extends AbstractIntegrationTest` dispondrán de los servicios dockerizados para operar con ellos, sin necesidad de que ninguno esté marcado con `@DirtiesContext`, aunque marcándolo también funcionaría (pagando el coste de reiniciar el contexto, junto con los servicios dockerizados).

![all-green](https://dev-to-uploads.s3.amazonaws.com/i/2kip4aysdtc6gi3o8krh.png)
 
:rocket:

## Conclusión

TestContainers es una herramienta muy potente para evitar que perdamos tiempo simulando / mockeando lo que debería hacer nuestra capa de integración con infraestructura a la hora de desarrollar los tests y es bueno aprovecharse de ello!

Aún así, no siempre hay una manera de implementar las cosas y es mejor adaptarse a la que mejor nos convenga en cada caso con conocimiento de las herramientas que estamos usando, y cuando usamos un framework como Spring, éste dispone para nosotros muchas facilidades para agilizar los desarrollos, tanto de la aplicación, como en este caso de los tests.

> _Y llegados a este punto, no queda mucho más que decir por mi parte, espero que os haya gustado este viaje a las entrañas de Spring en el contexto de testing, así que si es el caso o si gestionáis el ciclo de vida de los contenedores de test de alguna otra forma y queréis compartirla dejad un comentario, que me gustará leeros._

Y ahora sí, fin :)