2

Good day. My Spring Boot app uses Postgress database. For tests it uses H2 database. When running in non-test mode beans need to be initialized in this order:

1) Init DataSource
2) Init JPA beans

When running in test mode I need to create and populate H2 database before JPA beans initialization:

1) Init DataSource
2) Init DataSourceInitializer
3) Init JPA beans

The problem is that JPA beans get initialized before DataSourceInitializer (step 3 precedes step 2) and test fails on missing tables (hibernate.hbm2ddl.auto=validate).

Step 1

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "datasource.runtime")
    public DataSource runtimeDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Step 2

@Configuration
@Profile(Profiles.INTEGRATION_TEST)
public class DataSourceTestConfig {

    @Autowired
    private ResourceLoader resourceLoader;

    @Bean
    public DataSourceInitializer runtimeDataSourceInitializer(@Qualifier("runtimeDataSource") DataSource dataSource) {
        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource);
        initializer.setDatabasePopulator(new ResourceDatabasePopulator(
                resourceLoader.getResource("classpath:runtime/schema.sql")
        ));
        return initializer;
    }
}

Step 3

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Autowired
    private Environment environment;

    @Autowired
    @Qualifier(value = "runtimeDataSource")
    private DataSource runtimeDataSource;

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean runtimeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(runtimeDataSource)
                .properties(hibernateSettings())
                .packages(
                        "cz.adx.anx.car.cases.domain",
                        "cz.adx.anx.car.lib.domain",
                        "org.springframework.data.jpa.convert.threeten" // Hibernate support for Java 8 date and time classes
                )
                .persistenceUnit("runtimePersistenceUnit")
                .build();
    }
}

I need beans from class DataSourceTestConfig get initialized before JpaConfig and after DataSourceConfig but only in test mode. In non-test mode beans from JpaConfig should be initialized after DataSourceConfig and beans from DataSourceTestConfig must be omited. Therefore I cannot annotate class JpaConfig with @DependsOn beans from class DataSourceTestConfig because this class is located in test packages and not present in non-test mode. I could duplicate config classes and make them conditional on profile but I don't feel comfortable with this solution. Please, is there a better solution? Thanks in advance!

PS: My app uses two databases/datasources but I shortened the code above to make it easier to read. I'm using Spring Boot 1.3.1.RELEASE.

UPDATE 1: I tried to use approach suggested by @luboskrnac. I placed annotation ActiveProfiles on my integration test classes:

@ActiveProfiles("IT")
public abstract class IntegrationTest {...}

And I used annotation Profile on relevant beans in class JpaConfig shown below:

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Autowired
    private Environment environment;

    @Autowired
    @Qualifier(value = "runtimeDataSource")
    private DataSource runtimeDataSource;

    @Autowired
    @Qualifier(value = "configDataSource")
    private DataSource configDataSource;

    @Profile("!IT")
    @Bean(name = "runtimeEntityManagerFactory")
    @DependsOn("runtimeDataSource")
    public LocalContainerEntityManagerFactoryBean runtimeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return createRuntimeEntityManagerFactory(builder);
    }

    @Profile("IT")
    @Bean(name = "runtimeEntityManagerFactory")
    @DependsOn("runtimeDataSourceInitializer")
    public LocalContainerEntityManagerFactoryBean testRuntimeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return jpaConfig.createRuntimeEntityManagerFactory(builder);
    }

    public LocalContainerEntityManagerFactoryBean createRuntimeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
            .dataSource(runtimeDataSource)
            .properties(hibernateSettings())
            .packages(
                "cz.adx.anx.car.cases.domain",
                "cz.adx.anx.car.lib.domain",
                "org.springframework.data.jpa.convert.threeten" // Hibernate support for Java 8 date and time classes
            )
            .persistenceUnit("runtimePersistenceUnit")
            .build();
    }
}

And I'm creating the transaction managers the same way. Because I use two datasources (two different databases) I use bean names in the EnableJpaRepositories annotation.

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "runtimeEntityManagerFactory",
        transactionManagerRef = "runtimeTransactionManager",
        basePackages = "cz.adx.anx.car.lib.repository"
)
public class JpaCarLibRepositoryConfig {
}

So I need the non-test bean and test bean registered under the same name. But Spring gives me an exception:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.persistence.EntityManagerFactory] is defined: expected single matching bean but found 2: runtimeEntityManagerFactory

Any advices please?

Vojtech
  • 2,533
  • 9
  • 34
  • 65
  • Are these unit tests or integration tests? For unit tests I would use mockito and mock all the dependencies and create the unit tests that way without ever connecting to a database. – Cameron Chapman Feb 06 '16 at 15:06
  • 1
    When composing @Configuration(s) the @Import(ed) ones are initialized in order specified in the @Import. That alone shall allow for creating high-level configurations including either two or three low-level configurations and having initialized the low-level configurations in any desired order. – Michal Feb 06 '16 at 20:33
  • These are integration tests. – Vojtech Feb 07 '16 at 08:10

1 Answers1

1

I would suggest to drop any considerations about explicit bean creation ordering or bean dependencies.

Simply populate database in test based on Spring @Sql annotation. Test may look something like this:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Sql("/test-schema.sql")
public class DatabaseTests {

    @Test
    public void emptySchemaTest {
        // execute code that uses the test schema without any test data
    }

    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    public void userTest {
        // execute code that uses the test schema and test data
    }
}

If you'll need to swap datasource (e.g. using PostgereSQL in PROD and H2 in tests), just use Spring @Profile, @ActiveProfiles annotations.

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
  • Thanks for the reply. I tried suggested approach but I got into troubles. I updated my question, please see UPDATE 1. – Vojtech Feb 07 '16 at 11:39
  • Can you try to remote @DependsOn annotations? Otherwise it looks OK to me. – luboskrnac Feb 07 '16 at 13:38
  • I removed @DependsOn annotations but the same problem persists: "expected single matching bean but found 2..." – Vojtech Feb 08 '16 at 09:31
  • All right, the problem was elsewhere. As I use two EntityManagerFactoryBeans I needed to put @Primary anotation on one of them. The solution with profiles works fine. – Vojtech Feb 08 '16 at 12:48