Search code examples
postgresqlspring-bootintegration-testingh2spring-boot-3

Sping Boot 3.1.2: ITs fail to setup H2/Postgres with SpringBootTest but DataJpaTest works


base data: Using JDK 17, I'm trying to update from Spring Boot 2.7.11 -> 3.1.2. We're using a Postgres (version: 42.6.0) in production, that's why we're using h2 (version 2.1.214) with MODE=PostgreSQL, but no explicit dialect setting.

The application consists of multiple modules, one which provides access to the persistence layer. It defines multiple entities, for which the database is setup using Liquibase (version: 4.23.0). The tests override this setting, having Liquibase setup the DB, before Hibernate drops the tables and re-creates them (create-drop).

One of our integration test looks as follows:

package my.service;

import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
class MyInternalServiceTest {

    @Autowired
    private MyInternalService myInternalService;

    @Test
    public void aVeryWellDesignedTest() {
        /* something */
    }

    @ComponentScan(basePackages = "my")
    @EnableJpaRepositories(basePackages = "my.persistence")
    @EntityScan(basePackages = "my.persistence")
    @SpringBootApplication
    @Profile("test")
    @PropertySource("classpath:application-test.yml")
    static class TestConfiguration {}
}

application-test.yml:

spring:
  datasource:
    driver-class-name: org.h2.Driver
    username: sa
    password:
    url: jdbc:h2:mem:tests;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;DB_CLOSE_ON_EXIT=FALSE

  jpa:
    hibernate:
      ddl-auto: create-drop
  main:
    allow-bean-definition-overriding: true

With Spring 2.7.11 the test boots up, the DB gets created using Liquibase, re-created using Hibernate and the test then does its job - all good.

If I now change to Spring Boot 3.1.2, Hibernate somehow cannot properly detect the target DB. It seems to use statements, that are not suitable to our H2/Postgres environment. E.g., it tries to create columns using type TINYINT for enumeration attributes or BIGINT for Long. It also cannot delete tables, that are referenced by foreign keys (it cannot remove those FKs) and sequences are also not created - or cannot be used, because it uses the wrong statement to obtain the next value. For the table creation statement, I get for example:

UPDATE SUMMARY
Run:                         73
Previously run:               0
Filtered out:                 0
-------------------------------
Total change sets:           73

Liquibase: Die Update-Operation war erfolgreich.
2023-08-13T09:21:50.571+02:00  WARN 10865 --- [main] o.h.t.s.i.ExceptionHandlerLoggedImpl     : GenerationTarget encountered exception accepting command : Error executing DDL "create table inouthistory (active boolean, geoeventsource integer, geofencestatus tinyint check (geofencestatus between 0 and 5), geotimestamp timestamp(6), id bigint not null, lastupdatetime timestamp(6), vehicleid bigint, primary key (id))" via JDBC [Unbekannter Datentyp: "TINYINT"
Unknown data type: "TINYINT";]

Note: "Liquibase: Die Update-Operation war erfolgreich." means something like "Liquibase: The Update-Operation finished successfully."

This gets resolved though, when I change @SpringBootTest to @org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest: the tables can be dropped and re-created by Hibernate (the test fails during the application context setup though).

To me, this seems to be a bug, since @DataJpaTest is supposed to be used for repository tests only - not for ITs. This may also be the reason, why I'm having trouble booting the application context. But the Spring Boot guys rejected this idea (https://github.com/spring-projects/spring-framework/issues/31043) - so I've probably missed something here (but I definitely liked how quickly a response came in).

This issue might be the cause for Spring Boot 3.1.2: Integration tests try to add @Configuration classes from other ITs to app context - but that's guessing, not knowing.

Please do not hesitate to ask for anything, if it might help you understand/resolve the issue.

Thanks. kniffte


Solution

  • we finally found a working solution. It seems like the tests found multiple TestConfiguration classes (basically, each Test class has its own).

    We therefore created class

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.FilterType;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @ComponentScan(basePackages = "com.exxeta.dfstp.geofence",
        excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = {
            ".*TestConfiguration"
        }))
    public @interface TestComponentScan {}
    
    

    which is now used by our test class

    package my.service;
    
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.domain.EntityScan;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Profile;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    @ActiveProfiles("test")
    @Import({ MyInternalServiceTest.TestConfiguration.class })
    @SpringBootTest
    class MyInternalServiceTest {
    
        @Autowired
        private MyInternalService myInternalService;
    
        @Test
        public void aVeryWellDesignedTest() {
            /* something */
        }
    
        @TestComponentScan
        @SpringBootApplication
        @PropertySource("classpath:application-test.yml")
        static class TestConfiguration {}
    }