Después de un tiempo de reflexión acerca de cómo enfocar ciertos test de integración, buscando una herramienta que nos ofrezca un entorno aislado y configurable, he encontrado el camino para uno de los proyectos en los que trabajo.

Proceso de estudio

Cuando inicié el estudio de esta librería fue por un motivo que nos venía pasando con cierta frecuencia. Algunas inserciones de datos en ciertos entornos no se estaban comportando de la forma esperada, bien sea por ser entornos abiertos a mucha gente que puede añadir pruebas conceptualmente incorrectas o bien por ingesta de datos a través de scripts directamente en BBDD (los entornos no son nuestros).

Y la manera correcta de comprobar esto no es esperar a desplegar el desarrollo y ver cómo se comporta, sino ver qué ocurriría previamente mientras estamos desarrollando. Esto no descubre la pólvora, debería ser parte de nuestro día a día, pero hay proyectos en los que es difícil este tipo de iniciativas, y los test integrados siempre han sido un dolor de cabeza.

Y aquí es donde ha entrado Testcontainers, una librería que nos levanta una imagen Docker con una instancia de la BBDD que nos interese. Esto nos subsana 2 cosas:

  1. Nos elimina la limitación de las BBDD en memoria.
  2. Nos replica el entorno de producción y nos aísla de su estado.

El problema es que en mi proceso de investigación de esta librería no me quedaba muy claro cómo hacer esto y, una vez uno se pega con ello, entiende cómo enfocarlo. Por otro lado, he visto en Internet muchísimos ejemplos, pero que creo que no tenían muy claro qué probar, eso, o me he perdido algo.

En este post, os lo voy a intentar exponer con un caso práctico, tanto lo que propongo con esta solución como lo que he visto en la red (que como digo no ha sido un caso aislado).

¿Qué nos ofrece Testcontainers?

Testcontainers es una biblioteca de Java que admite pruebas JUnit, que proporciona instancias desechables y livianas de bases de datos comunes, navegadores web Selenium, o cualquier otra cosa que pueda ejecutarse en un contenedor Docker. Además, tenemos la posibilidad de poder usar varios contenedores, por ejemplo, si una prueba necesita ejecutar algún otro contenedor de Docker que necesite acceso a Kafka, puede ejecutar otro contenedor en la misma red que el contenedor de Kafka.

Se puede hacer pruebas de integración de la capa de acceso a datos, utilizando una instancia en un contenedor de una base de datos MySQL, PostgreSQL u Oracle, sin requerir una configuración compleja en las máquinas de los desarrolladores. Además, con la certeza de que sus pruebas siempre comenzarán con un estado de base de datos conocido.

Es decir, nos va a permitir aislar los entornos productivos de los entornos de pruebas de una forma sencilla:

Caso práctico

Antes de comenzar deberemos de tener instalado un Docker Desktop y descargarnos las siguientes imágenes, ya que nuestro caso va a ser sobre una Oracle con entidades JPA.

docker pull gvenzl/oracle-xe

11: Pulling from gvenzl/oracle-xe
e4430e06691f: Pulling fs layer
18713efb0b6f: Pulling fs layer
e4430e06691f: Verifying Checksum
e4430e06691f: Download complete
e4430e06691f: Pull complete
18713efb0b6f: Verifying Checksum
18713efb0b6f: Download complete
18713efb0b6f: Pull complete
Digest: sha256:a96c91d31b8e1ad91387d5d687987159115a63abef9d85a3f18b8fca7dda0fba
Status: Downloaded newer image for gvenzl/oracle-xe
docker.io/gvenzl/oracle-xe

docker pull Testcontainers/ryuk:0.3.3

En este caso práctico he generado una entidad llamada PaymentMethod que va a almacenar medios de pago y la capa de repositorio que se va a encargar de manejar dicha entidad (lo que viene siendo para hacer un CRUD de toda la vida). Y todo ello generado y ejecutado con TestContaiers y Junit-Jupiter

En el pom.xml necesitaremos lo siguiente:

<dependency>
   <groupId>org.Testcontainers</groupId>
   <artifactId>Testcontainers</artifactId>
   <version>${Testcontainers.version}</version>
   <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.Testcontainers/oracle-xe -->
<dependency>
   <groupId>org.Testcontainers</groupId>
   <artifactId>oracle-xe</artifactId>
   <version>${Testcontainers.version}</version>
   <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.Testcontainers/oracle-xe -->
<dependency>
   <groupId>org.Testcontainers</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>${Testcontainers.version}</version>
   <scope>test</scope>
</dependency>

El test lo ejecutaremos desde un contexto Spring para levantar los componentes. La prueba es muy sencilla, insertar un medio de pago y listarlo, sin más.

@Testcontainers
/**You are defining junit.jupiter.testinstance.lifecycle.default=per_class in your junit-platform.properties. Setting this back to per_method or just delete the line would fix your problem.
 Testcontainers JUnit extension does not really play well with this setting if you do not start to take control over the container lifecycle by yourself.
 But mixing with with the strict restriction of having a static method for the @DynamicPropertySource would require to start the container upfront the properties registration. This looks, at least to me, not ideal for a clean test structure.
 **/
@TestInstance(Lifecycle.PER_METHOD)
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = {
        ExampleApplication.class,
        OracleTestProfileJPAConfig.class})
public class ExampleApplicationTest {
    @Autowired
    private PaymentMethodRepository paymentMethodRepository;
    @Test
    void contextLoads() {
        System.out.println("Context loads!");
        PaymentMethod paymentMethod= new PaymentMethod();
        paymentMethod.setId(2L);
        paymentMethod.setCode("099");
        paymentMethod.setDescription("XIAOMI");
        paymentMethodRepository.save(paymentMethod);
        List<PaymentMethod> paymentMethodList= (List<PaymentMethod>) paymentMethodRepository.findAll();
        paymentMethodList.stream().forEach(paymentMethodAux -> {
            System.out.println("Payment method:"+paymentMethodAux.getDescription());
        });
    }
}

Bien, lo que yo he visto en la red es directamente esto (no tienen importado la clase OracleTestProfileJPAConfig):

@SpringBootTest(classes = {
        ExampleApplication.class,
        OracleTestProfileJPAConfig.class})

Es decir, nuestro dataSource del test apuntará a dónde esté apuntando nuestra aplicación. De hecho hice una prueba de inserción y listado y me salían los medios de pago dados de alta en mi BBDD local (lógico). De hecho, cuando levanto el contenedor tengo asociado un script para crear la tabla, así que en todo caso debería de dar de alta un registro y listarlo, pero no los demás medios de pago.

¿Y qué tiene la clase OracleTestProfileJPAConfig? Pues aquí es donde está la “chicha”.

Declaramos el contenedor de Oracle e inicializamos con el script que tendremos en nuestra carpeta de resources. Con withReuse le indicamos que el contenedor será reusable, en caso de no marcarlo el contenedor se volverá a levantar con cada test (lo cual no es lo más óptimo).

@Container
public static OracleContainer container = new OracleContainer("gvenzl/oracle-xe")
        .withInitScript("populateDB.sql")
        .withReuse(true);

Inicializamos el contenedor cuando declaramos el nuevo dataSource (que apuntará a la BBDD del contenedor).

@Bean
public DataSource dataSource() {
    container.start();
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl(container.getJdbcUrl());
    dataSource.setUsername(container.getUsername());
    dataSource.setPassword(container.getPassword());
    return dataSource;
}

Como podemos observar, el OracleContainer ya tiene predefinido un user/password, un dataBaseName etc.

public OracleContainer(DockerImageName dockerImageName) {
    super(dockerImageName);
    this.databaseName = "xepdb1";
    this.username = "test";
    this.password = "test";
    this.usingSid = false;
    dockerImageName.assertCompatibleWith(new DockerImageName[]{DEFAULT_IMAGE_NAME});
    this.preconfigure();
}

Y a continuación declaramos el entityManager de la siguiente manera (no os preocupéis, os dejo el enlace de la POC).

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource());
    em.setPackagesToScan(new String[] { "com.eci.pocTestcontainers.ms.example.repository" });
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    em.setJpaProperties(additionalProperties());
    return em;
}
@Bean
JpaTransactionManager transactionManager(final EntityManagerFactory entityManagerFactory) {
    final JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}
final Properties additionalProperties() {
    final Properties hibernateProperties = new Properties();
    hibernateProperties.setProperty("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
    hibernateProperties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
    hibernateProperties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
    return hibernateProperties;
}

Y este sería el resultado de la prueba:

Context loads!
Hibernate: select paymentmet0_.ID as id1_0_0_, paymentmet0_.CODE as code2_0_0_, paymentmet0_.DESCRIPTION as description3_0_0_ from PAYMENTMETHOD paymentmet0_ where paymentmet0_.ID=?
Hibernate: insert into PAYMENTMETHOD (CODE, DESCRIPTION, ID) values (?, ?, ?)
Hibernate: select paymentmet0_.ID as id1_0_, paymentmet0_.CODE as code2_0_, paymentmet0_.DESCRIPTION as description3_0_ from PAYMENTMETHOD paymentmet0_
Payment method:XIAOMI

Conclusiones

Como vemos Testcontainers aparte del caso expuesto, aquí contiene múltiples posibilidades de integrar pruebas levantando un contenedor. No he entrado más a fondo con otros frameworks con los que me gustaría probar (MongoDB, Kafka, RabbitMQ...) pero creo que una vez se tiene configurado de una forma pensada y bien integrada, es una librería muy potente que pueda dar salida a diferentes problemáticas

Para acceder a esta POC basta con que vayáis a este repo.

¿Hasta la próxima!! ¿Tienes alguna duda? ¡Déjanos un comentario!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.