Issue
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?
Solution
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.
Answered By - luboskrnac
Answer Checked By - Katrina (JavaFixing Volunteer)