1. Descripción general
Con Spring Data JPA, puede crear fácilmente consultas en la base de datos y probarlas utilizando la base de datos H2 incorporada.
Pero a veces las pruebas en una base de datos real son mucho más útiles , especialmente si usamos consultas vinculadas a una implementación de base de datos específica.
En esta guía, mostraremos cómo usar Testcontainers para las pruebas de integración con Spring Data JPA y la base de datos PostgreSQL.
En el artículo anterior, creamos varias consultas de bases de datos, utilizando principalmente la anotación @Query , que estamos probando ahora.
2. Configuración
Para usar la base de datos PostgreSQL en nuestras pruebas, debemos agregar la dependencia Testcontainers de solo pruebas y el controlador PostgreSQL a nuestro archivo pom.xml :
<dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.10.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
También crearemos el archivo application.properties en el directorio de recursos de prueba, en el que estableceremos el uso de la clase de controlador requerida para Spring, así como crearemos y eliminaremos el esquema de la base de datos cada vez que se ejecute la prueba:
spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop
3. Prueba unitaria
Para comenzar a usar una instancia de PostgreSQL en una sola clase de prueba, debe crear una definición de contenedor y luego usar sus parámetros para establecer la conexión:
@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class}) public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests { @ClassRule public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1") .withDatabaseName("integration-tests-db") .withUsername("sa") .withPassword("sa"); static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public void initialize(ConfigurableApplicationContext configurableApplicationContext) { TestPropertyValues.of( "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(), "spring.datasource.username=" + postgreSQLContainer.getUsername(), "spring.datasource.password=" + postgreSQLContainer.getPassword() ).applyTo(configurableApplicationContext.getEnvironment()); } } }
En el ejemplo anterior, usamos @ClassRule
de JUnit para configurar el contenedor de la base de datos antes de ejecutar los métodos de prueba . También creamos una clase interna estática que implementa ApplicationContextInitializer . Finalmente, aplicamos la anotación @ContextConfiguration
a nuestra clase de prueba con la clase de inicialización como parámetro.
Después de completar estos tres pasos, podemos establecer los parámetros de conexión antes de publicar el contexto Spring.
Ahora usamos dos consultas de ACTUALIZACIÓN del artículo anterior:
@Modifying @Query("update User u set u.status = :status where u.name = :name") int updateUserSetStatusForName(@Param("status") Integer status, @Param("name") String name); @Modifying @Query(value = "UPDATE Users u SET u.status = ? WHERE u.name = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
Y pruebe en un entorno de tiempo de ejecución sintonizado:
@Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = userRepository.updateUserSetStatusForName(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } @Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } private void insertUsers() { userRepository.save(new User("SAMPLE", "email@example.com", 1)); userRepository.save(new User("SAMPLE1", "email2@example.com", 1)); userRepository.save(new User("SAMPLE", "email3@example.com", 1)); userRepository.save(new User("SAMPLE3", "email4@example.com", 1)); userRepository.flush(); }
En el escenario anterior, la primera prueba tiene éxito y la segunda arroja una InvalidDataAccessResourceUsageException con el mensaje:
Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist
Si ejecutamos las mismas pruebas utilizando la base de datos H2 incorporada, ambas serían exitosas, pero PostgreSQL no acepta alias en la instrucción SET. Podemos solucionar rápidamente la solicitud eliminando el alias problemático:
@Modifying @Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
Esta vez ambas pruebas pasaron con éxito. En este ejemplo, usamos Testcontainers para identificar un problema con una consulta nativa que de otra manera se detectaría solo después de ir a la base de datos de producción . También debe tenerse en cuenta que usar consultas JPQL es generalmente más seguro, ya que Spring las traduce correctamente dependiendo del proveedor de la base de datos utilizado.
4. Instancia de base de datos compartida
En la sección anterior, describimos cómo usar Testcontainers en una sola prueba. En casos reales, me gustaría usar el mismo contenedor de base de datos en varias pruebas debido al tiempo de lanzamiento relativamente largo.
Creemos una clase común para crear un contenedor de base de datos heredando PostgreSQLContainer y anulando los métodos start () y stop () :
public class BaeldungPostgresqlContainer extends PostgreSQLContainer<BaeldungPostgresqlContainer> { private static final String IMAGE_VERSION = "postgres:11.1"; private static BaeldungPostgresqlContainer container; private BaeldungPostgresqlContainer() { super(IMAGE_VERSION); } public static BaeldungPostgresqlContainer getInstance() { if (container == null) { container = new BaeldungPostgresqlContainer(); } return container; } @Override public void start() { super.start(); System.setProperty("DB_URL", container.getJdbcUrl()); System.setProperty("DB_USERNAME", container.getUsername()); System.setProperty("DB_PASSWORD", container.getPassword()); } @Override public void stop() {
Dejando el método stop () vacío, permitimos que la JVM maneje la finalización del contenedor por sí mismo. También implementamos un singleton simple, en el que solo la primera prueba inicia el contenedor, y cada prueba posterior utiliza una instancia existente. En el método start () , usamos System # setProperty para guardar los parámetros de conexión en las variables de entorno.
Ahora podemos escribirlos en el archivo application.properties :
spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD}
Ahora usamos nuestra clase de utilidad en la definición de prueba:
@RunWith(SpringRunner.class) @SpringBootTest public class UserRepositoryTCAutoIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();
Como en los ejemplos anteriores, aplicamos la anotación @ClassRule al campo con la definición del contenedor. Por lo tanto, los parámetros de conexión de DataSource se rellenan con los valores correctos antes de crear el contexto Spring.
Ahora podemos implementar varias pruebas usando la misma instancia de base de datos simplemente configurando el campo de anotación @ClassRule creado usando nuestra clase de utilidad BaeldungPostgresqlContainer .
5. Conclusión
En este artículo, mostramos métodos de prueba en una base de datos de producción usando Testcontainers.
También examinamos ejemplos de uso de una sola prueba usando el mecanismo ApplicationContextInitializer de Spring, así como la implementación de una clase para reutilizar una instancia de base de datos.
También mostramos cómo Testcontainers puede ayudar a identificar problemas de compatibilidad entre múltiples proveedores de bases de datos, especialmente para consultas nativas.
Como siempre, el código completo utilizado en este artículo está disponible en GitHub .