viernes, 12 de mayo de 2023

Testcontainers: Uso básico

Ya hace 3 años, sobre Agosto de 2020 vi un post sobre cómo realizar pruebas unitarias con contenedores. Y aunque tuve la intención de hacer un post, nunca llegue a hacerlo. Pero ahora tras ver cómo funcionan las pruebas con Quarkus, del cual intentaré hablar más adelante, me he interesado en sacar este post. 

En múltiples de los posts que he realizado, he utilizado siempre contenedores para poder realizar las pruebas, y poder comprobar el ejemplo práctico. Normalmente dejando un docker-compose.yml que permitiese levantar y configurar los sistemas externos con los cuales iba a trabajar la aplicación. 

Ahora vamos a hacer algo parecido Y para llevarlo a cabo utilizaremos Testcontainers. Es una librería Java que nos permite crear instancias de Docker y manejarlas en base a nuestro interés. En el ejemplo utilizaremos las siguientes librerías:

  • org.testcontainers:testcontainers:1.16.0
  • org.testcontainers:junit-jupiter:1.16.0
  • org.testcontainers:mysql:1.16.0
  • org.apache.camel:camel-test-spring-junit5:3.16.0
  • org.springframework.boot:spring-boot-starter-test:2.5.1

Para empezarla a utilizar, usaremos principalmente dos anotaciones:

  • @Testcontainers: que nos permite utilizar Testcontainers con JUnit 5.
  • @Containers: Nos permite indicar a las instancias de contenedores que queremos que sean gestionadas por Testcontainers. Aunque como veremos más adelante podemos utilizar Testcontainers sin ella. 
Antes de empezar debemos aclarar un poco el funcionamiento. Testcontainers nos permite levantar gestionar contenedores de cualquier imagen que queramos. Pudiéndole añadir a través de sus métodos una breve configuración. Pero también tiene módulos específicos para hacer uso de contenedores comunes, como puede ser de BBDD, Hashicorp, RabitMQ o Kafka.

Otro asunto importante a tener en cuenta, que aunque nosotros les indiquemos los puertos que queremos exponer, no podremos crear bindings tal y como se realiza en un docker-compose. Sino que los puertos expuestos serán enlazados con puertos aleatorios de nuestra máquina. Debido a esto, deberemos modificar antes de realizar las pruebas la configuración que tenemos por defecto. Para que podamos indicar los puertos que utilizarán los sistemas, una vez han sido arrancados con Testcontainers. 

Para ver cómo funciona, utilizaremos ejemplos hechos con Apache Camel y Spring Boot. En el primer ejemplo enviaremos y recibiremos mensajes de una cola de ActiveMQ. A continuación detallo los componentes más importantes del ejemplo:
  • La instancia de GenericContainer nos permitirá indicar la imagen de ActiveMQ a crear y los puertos a exponer.
  • Crear un método BeforeAll que permita arrancar el contenedor y el puerto vinculado externo. 
  • Modificar la configuración de los componentes que realizan la comunicación con el sistema externo. En Spring Boot lo podemos hacer fácilmente con un método anotado con @DynamicPropertySource.
@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = ApacheCamelTestApplication.class)
@Log4j2
public class ApacheActiveMqRouterTest {
    @Autowired
    ProducerTemplate producer;
    @Container
    private static GenericContainer container = new GenericContainer("rmohr/activemq").withExposedPorts(61616, 8161);
    private static Integer tcpPort;
    @BeforeAll
    public static void beforeAll() {
        container.start();
        tcpPort = container.getMappedPort(61616);
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + tcpPort);
    }
    @Test
    public void amqTo01() throws InterruptedException {
        producer.sendBody("direct:SendToPublic", "mensaje ");
    }
}

En el segundo ejemplo, será similar en funcionamiento al anterior. Con la excepción de qué utilizaremos un módulo concreto de Testcontainers. Con él, a través de la clase MySQLContainer podremos hacer configuraciones específicas para crear el contenedor. Como indicar un script para inicializar la base de datos. 

@Testcontainers
@CamelSpringBootTest
@SpringBootTest(classes = {ApacheCamelTestApplication.class})
@Slf4j
public class ApacheMySQLRouterTest{
    static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @Autowired
    ProducerTemplate producer;
@BeforeAll public static void beforeAll() throws IOException { database.start(); } @DynamicPropertySource static void databaseProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", database::getJdbcUrl); registry.add("spring.datasource.username", database::getUsername); registry.add("spring.datasource.password", database::getPassword); } @Test public void getBookById() throws InterruptedException { producer.sendBodyAndHeader("direct:getBookById", null, "id", 1); } }

Antes de terminar dos cosas. El primero, es que durante la realización de los ejemplos he tenido problemas con distintas versiones de Testcontainers: 
  • 1.18: Genero la excepción NoSuchMethodError asociada al método optionallyMapResourceParameterAsVolume. 
  • 1.17: Genero ClassNotFoundException: asociado a la clase org.testcontainers.shaded.org.apache.commons.lang.StringUtils
Y lo segundo, es que sí debemos utilizar un varios contenedores como es en este caso podemos tener errores debido a que nuestras aplicación se intente conectar a un sistema externo y no tengamos su contenedor levantado. Por tanto como recomendación, podemos crear una clase que contenga toda la configuración de los contenedores y de la que extenderemos. 

@SpringBootTest
@Testcontainers
@CamelSpringBootTest
@Slf4j
public class TestcontainersConf {
    public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.34");
    static GenericContainer<?> container = new GenericContainer<>("rmohr/activemq").withExposedPorts(61616, 8161);    
    static MySQLContainer<?> database = new MySQLContainer<>(MYSQL_57_IMAGE)
    .withInitScript("scripts/init_mysql.sql")
    .withDatabaseName("library").withLogConsumer(new Slf4jLogConsumer(log));
    @BeforeAll
    public static void beforeAll() {
        container.start();
        database.start();
    }
    @DynamicPropertySource
    static void replaceProperties(DynamicPropertyRegistry registry) {
        registry.add("activemq.broker-url", () -> "tcp://localhost:" + container.getMappedPort(61616));
        registry.add("spring.datasource.url", database::getJdbcUrl);
        registry.add("spring.datasource.username", database::getUsername);
        registry.add("spring.datasource.password", database::getPassword);
    }
}
// ............................
public class ApacheMySQLRouterTest extends TestcontainersConf{
    @Autowired
    ProducerTemplate producer;
    @Test
    public void getBookById() throws InterruptedException {
        producer.sendBodyAndHeader("direct:getBookById", null, "id", 1);
    }
}

Espero que os haya ayudado, y si os interesa, aquí tenéis todo el código fuente. 

No hay comentarios:

Publicar un comentario