darefilz
darefilz

Reputation: 701

How to use autowired repositories in Spring Batch integration test?

I am facing some issues while writing integration tests for Spring Batch jobs. The main problem is that an exception is thrown whenever a transaction is started inside the batch job.
Well, first things first. Imagine this is the step of a simple job. A Tasklet for the sake of simplicity. Of course, it is used in a proper batch config (MyBatchConfig) which I also omit for brevity.

@Component
public class SimpleTask implements Tasklet {

    private final MyRepository myRepository;

    public SimpleTask(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        myRepository.deleteAll(); // or maybe saveAll() or some other @Transactional method
        return RepeatStatus.FINISHED;
    }
}

MyRepository is a very unspecial CrudRepository.

Now, to test that job I use the following test class.

@SpringBatchTest
@EnableAutoConfiguration
@SpringJUnitConfig(classes = {
    H2DataSourceConfig.class, // <-- this is a configuration bean for an in-memory testing database
    MyBatchConfig.class
})
public class MyBatchJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;
    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;
    @Autowired
    private MyRepository myRepository;

    @Test
    public void testJob() throws Exception {
        var testItems = List.of(
            new MyTestItem(1),
            new MyTestItem(2),
            new MyTestItem(3)
        );
        myRepository.saveAll(testItems); // <--- works perfectly well
        jobLauncherTestUtils.launchJob();
    }
}

When it comes to the tasklet execution and more precisely to the deleteAll() method call this exception is fired:

org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction; nested exception is java.lang.IllegalStateException: Already value [org.springframework.jdbc.datasource.ConnectionHolder@68f48807] for key [org.springframework.jdbc.datasource.DriverManagerDataSource@49a6f486] bound to thread [SimpleAsyncTaskExecutor-1]
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:448)
    ...

Do you have any ideas why this is happening?


As a workaround I currently mock the repository with @MockBean and back it with an ArrayList but this is not what the inventor intended, I guess.

Any advice?
Kind regards


Update 1.1 (includes solution)
The mentioned data source configuration class is

@Configuration
@EnableJpaRepositories(
        basePackages = {"my.project.persistence.repository"},
        entityManagerFactoryRef = "myTestEntityManagerFactory",
        transactionManagerRef = "myTestTransactionManager"
)
@EnableTransactionManagement
public class H2DataSourceConfig {

    @Bean
    public DataSource myTestDataSource() {
        var dataSource = new DriverManagerDataSource();

        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1");
        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean myTestEntityManagerFactory() {
        var emFactory = new LocalContainerEntityManagerFactoryBean();
        var adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabasePlatform("org.hibernate.dialect.H2Dialect");
        adapter.setGenerateDdl(true);

        emFactory.setDataSource(myTestDataSource());
        emFactory.setPackagesToScan("my.project.persistence.model");
        emFactory.setJpaVendorAdapter(adapter);
        return emFactory;
    }

    @Bean
    public PlatformTransactionManager myTestTransactionManager() {
        return new JpaTransactionManager(myTestEntityManagerFactory().getObject());
    }

    @Bean
    public BatchConfigurer testBatchConfigurer() {
        return new DefaultBatchConfigurer() {
            @Override
            public PlatformTransactionManager getTransactionManager() {
                return myTestTransactionManager();
            }
        };
    }
}

Upvotes: 1

Views: 1763

Answers (1)

Mahmoud Ben Hassine
Mahmoud Ben Hassine

Reputation: 31630

By default, when you declare a datasource in your application context, Spring Batch will use a DataSourceTransactionManager to drive step transactions, but this transaction manager knows nothing about your JPA context.

If you want to use another transaction manager, you need to override BatchConfigurer#getTransactionManager and return the transaction manager you want to use to drive step transactions. In your case, you are only declaring a transaction manager bean in the application context which is not enough. Here a quick example:

@Bean
public BatchConfigurer batchConfigurer() {
   return new DefaultBatchConfigurer() {
      @Override
      public PlatformTransactionManager getTransactionManager() {
          return new JpaTransactionManager(myTestEntityManagerFactory().getObject());
      }
   };
}

For more details, please refer to the reference documentation.

Upvotes: 1

Related Questions