0

I have this JPA configuration class which is in the regular classpath (not in test classpath) and I don't want to duplicate it (I don't want one copy for runtime, and one copy for test) :

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:persistence-${env:int}.properties")
@EnableJpaRepositories(basePackages = "fr.tristan.demoassurance.infrastructure.repository.mysql")
@SuppressWarnings("unused")
public class PersistenceJpaConfig {

    @Bean
    LocalContainerEntityManagerFactoryBean entityManagerFactory(Environment env) {
        final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource(env));
        em.setPackagesToScan("fr.tristan.demoassurance.infrastructure.repository.mysql.entity");

        final JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(additionalProperties(env));

        return em;
    }

    @Bean
    DataSource dataSource(Environment env) {
        final DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(Preconditions.checkNotNull(env.getProperty("jdbc.driverClassName")));
        dataSource.setUrl(Preconditions.checkNotNull(env.getProperty("jdbc.url")));
        dataSource.setUsername(Preconditions.checkNotNull(env.getProperty("jdbc.user")));
        dataSource.setPassword(Preconditions.checkNotNull(env.getProperty("jdbc.pass")));

        return dataSource;
    }

    @Bean
    PlatformTransactionManager transactionManager(Environment env) {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory(env).getObject());
        return transactionManager;
    }

    @Bean
    PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

    final Properties additionalProperties(Environment env) {
        final Properties hibernateProperties = new Properties();
        hibernateProperties.setProperty("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
        hibernateProperties.setProperty("hibernate.hbm2ddl.import_files", env.getProperty("hibernate.hbm2ddl.import_files"));
        hibernateProperties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        hibernateProperties.setProperty("hibernate.cache.use_second_level_cache", env.getProperty("hibernate.cache.use_second_level_cache"));
        hibernateProperties.setProperty("hibernate.cache.use_query_cache", env.getProperty("hibernate.cache.use_query_cache"));
        return hibernateProperties;
    }
}

It relies on 2 properties file, only one is loaded using -Denv=int or -Denv=dev (or just no param since "int" is the default env value) :

  • persistence-dev.properties
  • persistence-int.properties

If I put my H2 dependency in "test" scope instead of the default "compile" scope,

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>

and I run my tests using mvn clean verify -Denv=dev, I get this error :

2023-05-31T13:04:27.200+02:00 ERROR 17240 --- [           main] o.s.test.context.TestContextManager      : Caught exception while allowing TestExecutionListener [org.springframework.test.context.web.ServletTestExecutionListener] to prepare test instance [fr.tristan.demoassurance.DemoAssuranceServiceIntegrationTest@578483bd]
(...)
Caused by: java.lang.IllegalStateException: Could not load JDBC driver class [org.h2.Driver]
Tristan
  • 8,733
  • 7
  • 48
  • 96
  • oh is it ? I think the test scope is about the test being test classes or not being in test classes, not related to integration or unit test. Do u have a source for that ? – Tristan May 31 '23 at 12:45
  • I was not precise in my comment. Sorry. – Mar-Z May 31 '23 at 13:04

2 Answers2

0

Here is the solution I've found fulfilling all my requirements :

create a PersistenceJpaTestConfig in a package of "src/test/java" and make it extend the conf you don't want to duplicate :

@Configuration
public class PersistenceJpaTestConfig extends PersistenceJpaConfig {
}

Now you can put the h2 dependency in "test" scope because the config class using the H2 dependency is in target/test-classes :

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
Tristan
  • 8,733
  • 7
  • 48
  • 96
  • 1
    if you would like to favour composition over inheritance, you may use `@Import(PersistenceJpaConfig.class)` instead of `extends PersistenceJpaConfig`. – eHayik May 31 '23 at 19:47
  • is using `@Import` a "composition" though ? How would I override one of the bean method after using `@Import` ? – Tristan Jun 01 '23 at 06:05
  • [Enabling bean overriding feature and using `@TestConfiguration` instead of `@Configuration`](https://reflectoring.io/spring-boot-testconfiguration/) in your test configuration classes. – eHayik Jun 01 '23 at 14:30
0

You may use component based "composition".

Spring Testing Swiss knife packs @TestConfiguration to modify Spring’s application context during test runtime. You can use it to override certain bean definition within your tests.

In addition, you must enable bean overriding behavior:

spring.main.allow-bean-definition-overriding=true

The bean overriding feature is disabled by default from Spring Boot 2.1. A BeanDefinitionOverrideException is thrown if we attempt to override one or more beans.

Note that, There is no need to explicitly register PersistenceJpaConfig. However, If it isn't chosen by component scanning when the application context is built during your tests execution, You may add @Import(PersistenceJpaConfig.class) above your test configuration class name.

Furthermore, You have to explicitly register your @TestConfiguration class, using @ContextConfiguration on your test class or @SpringBootTest(classes = PersistenceJpaTestConfig.class).

For instance:

@TestConfiguration
// @Import(PersistenceJpaConfig.class) uncomment if, 
//it isn't chosen by component scanning when the application context is built.
public class PersistenceJpaTestConfig {

    @Bean
    public DataSource dataSource() {
        return mock(DataSource.class);
    }   
}

@SpringBootTest(
        classes = PersistenceJpaTestConfig.class,
        properties = "spring.main.allow-bean-definition-overriding=true")
class ApplicationTests {
   ....
}

All beans registered within PersistenceJpaConfig.class should be loaded, but dataSource bean should be overridden by PersistenceJpaTestConfig.dataSource().

You may find an example here.

sources:

eHayik
  • 2,981
  • 1
  • 21
  • 33
  • "There is no need to explicitly register PersistenceJpaConfig" : I extend it from a class in src/test/java so that "test" scope is enough for H2 dependency. – Tristan Jun 05 '23 at 14:15