Testeando una Spring Boot App dockerizada
Tags: showdev, springboot, docker, testing
Published: 2021-05-20 06:13:00

Nuestro compañero **Alex Castells** (@alextremp) en su post [Integrando Testcontainers en el contexto de Spring en nuestros tests](https://dev.to/adevintaspain/integrando-testcontainers-en-el-contexto-de-spring-en-nuestros-tests-5b7d) nos explicaba diferentes maneras para usar [Testcontainers](https://www.testcontainers.org/) en un test de Spring Boot.

Con **Daniel Dios** (@danieldios) queríamos llevarlo un paso más allá y usar Testcontainers para testear una Spring Boot Application una vez dockerizada.

Por eso hicimos un live coding que puedes ver en 💜[Twitch](https://www.twitch.tv/videos/1028060859) o ❤️[Youtube](https://www.youtube.com/watch?v=gx99aHEszp0)

Salvo algunos sustos 😰 ¡¡conseguimos nuestro objetivo 🤩!!

Puedes consultar el código en [AdevintaSpain/spring-boot-docker](https://github.com/AdevintaSpain/spring-boot-docker) y te resumimos los pasos a continuación:

[1. Crear una Spring Boot Application con un simple RestController](https://www.youtube.com/watch?v=gx99aHEszp0&t=538s)

Usamos [Spring Initialzr](https://start.spring.io/) para crear una Spring Boot Application y empezamos con un test del controlador que queremos implementar:

```kotlin
@SpringBootTest(webEnvironment = RANDOM_PORT)
class ApplicationTests {
  
  @LocalServerPort
  private var port: Int = 0
  
  @Test
  internal fun `should say hello`() {
    val webClient = WebClient.builder()
      .baseUrl("http://localhost:$port")
      .build()
    
    val actual = webClient
      .get()
      .uri("/hello")
      .exchangeToMono { it.bodyToMono(String::class.java) }
      .block()
    
    assertThat(actual).isEqualTo("hello Dani&Roger!")
  }
}
```

[2. Dockerizar la Spring Boot App](https://www.youtube.com/watch?v=gx99aHEszp0&t=1305s)

Existen varias alternativas que puedes consultar en [Spring Boot with Docker](https://spring.io/guides/gs/spring-boot-docker) y en más detalle en [Topical Guide on Docker](https://spring.io/guides/topicals/spring-boot-docker).

Podríamos simplemente crear un `Dockerfile` y hacer `docker build` pero optamos por usar la tarea `bootBuildImage` integrada en la Spring Boot gradle plugin. 

Por lo que entendemos esta tarea ya construye la docker image con el orden correcto de capas para asegurar que los elementos que cambian menos frecuentemente se incluyan en las primeras capas, minimizando así el tamaño y el tiempo de build.

```bash
./gradlew bootBuildImage
```

[3. Testear la Spring Boot App dockerizada](https://www.youtube.com/watch?v=gx99aHEszp0&t=1634s)

Usamos [Testcontainers](https://www.testcontainers.org/features/creating_container/#creating-a-generic-container-based-on-an-image) con un generic container y las anotaciones de [JUnit5](https://www.testcontainers.org/test_framework_integration/junit_5):

```kotlin
class KGenericContainer(imageName: String) : GenericContainer<KGenericContainer>(imageName)

@Testcontainers
class ContainerTest {
  
  companion object {
    private const val appPort = 8080
    
    @Container
    var app: KGenericContainer = KGenericContainer("spring-boot-docker:0.0.1-SNAPSHOT")
      .withExposedPorts(appPort)
  }
  
  @Test
  internal fun `should say hello`() {
     // ...
  }
```

[4. Ejecutar bootBuildImage antes del test](https://www.youtube.com/watch?v=gx99aHEszp0&t=2400s)

Para que siempre testeemos la última versión, necesitamos generar la docker image antes de ejecutar el test, lo conseguimos con el `dependsOn` de gradle:

```kotlin
tasks.withType<Test> {
  useJUnitPlatform()
  dependsOn("bootBuildImage")
}
```

[5. Test source root adicional](https://www.youtube.com/watch?v=gx99aHEszp0&t=2715s)

Bajo `test` deberíamos tener únicamente tests unitarios o máximo slice tests de Spring. Para nada tener tests que dependan de docker. Una manera de crear un test source root adicional para nuestros tests con docker es usar la gradle plugin [org.unbroken-dome.test-sets](https://plugins.gradle.org/plugin/org.unbroken-dome.test-sets):

```kotlin
plugins {
  id("org.unbroken-dome.test-sets") version "4.0.0"
}

testSets {
  create("container-test")
}

tasks["container-test"].dependsOn("bootBuildImage")
```
[6. Añadimos una base de datos](https://www.youtube.com/watch?v=gx99aHEszp0&t=3427s)

Para complicar un poco nuestra Spring Boot App añadimos una base de datos postgres y hacemos que nuestro `HelloController` ejecute una sencilla query. Como siempre empezamos por el test:

```kotlin
@Testcontainers
class ContainerTest {
  
  companion object {
    private const val postgresAlias = "mypostgres"
    private const val appPort = 8080
    private val network: Network = Network.newNetwork()
    
    @Container
    var postgres: KGenericContainer = KGenericContainer("postgres:13")
      .withNetwork(network)
      .withNetworkAliases(postgresAlias)
      .withEnv("POSTGRES_USER", "myuser")
      .withEnv("POSTGRES_PASSWORD", "mypassword")
      .withEnv("POSTGRES_DB", "mydb")
    
    @Container
    var app: KGenericContainer = KGenericContainer(System.getProperty("docker.image"))
      .withNetwork(network)
      .dependsOn(postgres)
      .withEnv("DB_HOST", postgresAlias)
      .withExposedPorts(appPort)
  }
  
  @Test
  internal fun `should say hello`() {
     // ...
  }
```

Puntos importantes:
* Ambos containers deben compartir la misma network porque queremos que nuestra app tenga conectividad interna con la base de datos.
* Añadimos un network alias `mypostgres` al container de la base de datos y el `dependsOn` en el container de la app para que desde éste último el hostname `mypostgres` se resuelva correctamente.
* Necesitamos exponer el puerto `8080` del container de la app porque queremos ejecutar una request desde el test, que estará corriendo fuera de docker.
* No necesitamos exponer ningún puerto del container de la base de datos.
* Sobreescribimos la propiedad `db.hostname` que usamos en nuestra Spring Boot App con la variable de entorno `DB_HOSTNAME` y el valor `mypostgres`. En el entorno real `db.hostname` tomará el valor correspondiente a la base de datos real.

[6. Convertimos el SpringBootTest original en un WebMvcTest](https://www.youtube.com/watch?v=gx99aHEszp0&t=6567s)

Queremos testear únicamente el `HelloController`, sin que requiera conexión con la base de datos.

[7. WebMvcTest no sirve ¡necesitamos WebFluxTest!](https://www.youtube.com/watch?v=gx99aHEszp0&t=7472s)

Unos momentos de pánico 😨 hasta que nos damos cuenta que estamos usando [WebFlux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) con lo que el slice test que debemos usar es `@WebFluxTest` y no `@WebMvcTest` 🤦‍♂️ ...

[8. WebTestClient y Kotlin ¡madre mía!](https://www.youtube.com/watch?v=gx99aHEszp0&t=8523s)

Pánico total 😱 no sabemos como implementar el `expectBody` con `WebTestClient` y Kotlin, al final lo salvamos con este workaround sacado de internet ... 😳:

```kotlin
@Test
internal fun `should say hello`() {
  val version = "Dummy 1.0"
  doReturn(version).`when`(helloRepository).getVersion()
  
  webClient
    .get().uri("/hello")
    .exchange()
    .expectStatus().is2xxSuccessful
    .expectBodyList(String::class.java)
    .consumeWith<WebTestClient.ListBodySpec<String>> {
      assertThat(it.responseBody?.get(0) ?: "xx").isEqualTo("Hello $version")
    }
}
```

[9. ¡Tests en verde!](https://www.youtube.com/watch?v=gx99aHEszp0&t=8570s)

Bueno al menos todos los tests están en verde 💚

Y ya fuera de cámaras, con más tranquilidad ...

**10. Así se usa el WebTestClient**

Tan fácil como esto, ¿en qué lío nos metimos? 😅

```kotlin
@Test
internal fun `should say hello`() {
  val version = "Dummy 1.0"
  doReturn(version).`when`(helloRepository).getVersion()
  
  webClient
    .get().uri("/hello")
    .exchange()
    .expectStatus().isOk
    .expectBody<String>()
    .isEqualTo("Hello $version")
}
```

**11. Versión de la docker image**

Para no tener fija la versión de la docker image de la app en el test de container, la pasamos como system property desde gradle:

```kotlin
testSets {
  val containerTest = create("container-test")
  containerTest.systemProperty("docker.image", "${project.name}:${project.version}")
}
```

Y la usamos en el test:

```kotlin
@Container
var app: KGenericContainer = KGenericContainer(System.getProperty("docker.image"))
```

Y eso es todo! Un saludiño de parte de @danieldios & @rogervinas  

[![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jtqwu2hebatsux2pa8l8.png)] (https://careers.adevinta.es/ofertas/?search=backend&stc=aff-blog%20dev.to-testeando%20una%20spring%20boot%20app%20dockerizada)