简体   繁体   中英

How to build Spring Boot Atomikos test configuration?

How to create proper testing environment to be able to use database layer tests and REST endpoints tests with mocks in same application?

I have a Spring Boot application with two data sources. To manage transactios Atomikos used. This config works fine.

Now I need to create tests. I build a test configuration and each test works fine, though when I run all test it fails. It seems to me (see stack trace) the problem is Atomikos cannot work if few Atomikos beans instantiated.

I tried two solutions to make Atomikos beans instantiated just once:

  1. Create one test config used for all tests (because Spring caches test context). But this not works. I think this is because Mock beans in @Controller break ability of reuse Spring test context. Persistence Mapper component I use in tests are mocked in one test and same time real instance used in other test. So I see each test class runs in it's own test context.

  2. Use @Lazy annotation at database @Configuration classes. As I thought this will ensure that bean will be instantiated only when called first time, and will be reused on further calls. But this not works either.

Here the sample project link I did to illustrate the issue. The repository includes MySQL database dump: https://github.com/pavelmorozov/AtomikosConfig

In this post I will show only one of two databases model, mapper and config classes as they are nearly same for second database.

java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83)
    at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:189)
    at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:131)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:539)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:761)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:461)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:207)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoController': Unsatisfied dependency expressed through field 'firstMapper'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstMapper' defined in file [/home/pm/Documents/workspace-sts-3.9.1.RELEASE/AtomikosConfig/target/classes/com/example/demo/persistence/mapper/first/FirstMapper.class]: Cannot resolve reference to bean 'firstSqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
    ... 25 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstMapper' defined in file [/home/pm/Documents/workspace-sts-3.9.1.RELEASE/AtomikosConfig/target/classes/com/example/demo/persistence/mapper/first/FirstMapper.class]: Cannot resolve reference to bean 'firstSqlSessionFactory' while setting bean property 'sqlSessionFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1531)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1276)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:585)
    ... 43 more
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firstSqlSessionFactory' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Unsatisfied dependency expressed through method 'firstSqlSessionFactory' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:467)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1173)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1067)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:513)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 56 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firstDataSource' defined in class path resource [com/example/demo/persistence/configuration/FirstDatabaseConfiguration.class]: Invocation of init method failed; nested exception is com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 66 more
Caused by: com.atomikos.jdbc.AtomikosSQLException: Cannot initialize AtomikosDataSourceBean
    at com.atomikos.jdbc.AtomikosSQLException.throwAtomikosSQLException(AtomikosSQLException.java:46)
    at com.atomikos.jdbc.AbstractDataSourceBean.init(AbstractDataSourceBean.java:306)
    at org.springframework.boot.jta.atomikos.AtomikosDataSourceBean.afterPropertiesSet(AtomikosDataSourceBean.java:49)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
    ... 77 more
Caused by: javax.naming.NamingException: Another resource already exists with name firstDataSource - pick a different name
    at com.atomikos.util.IntraVmObjectFactory.createReference(IntraVmObjectFactory.java:94)
    at com.atomikos.jdbc.AbstractDataSourceBean.getReference(AbstractDataSourceBean.java:388)
    at com.atomikos.jdbc.AbstractDataSourceBean.init(AbstractDataSourceBean.java:295)
    ... 80 more

.

@SpringBootApplication
@EnableAutoConfiguration(
        exclude = {DataSourceAutoConfiguration.class, 
                DataSourceTransactionManagerAutoConfiguration.class, 
                MybatisAutoConfiguration.class})
public class AtomikosConfigApplication {

    public static void main(String[] args) {
        SpringApplication.run(AtomikosConfigApplication.class, args);
    }
}

.

@Configuration
@Lazy
public class FirstDatabaseConfiguration {

    @Bean
    public MapperScannerConfigurer firstMapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.example.demo.persistence.mapper.first");
        configurer.setSqlSessionFactoryBeanName("firstSqlSessionFactory");
        return configurer;    
    }

    /**
     * This bean uses Atomikos
     * to get transaction atomicity for 
     * few data sources (distributed transaction)
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.first")
    public DataSource firstDataSource() {
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setPoolSize(10);     
        atomikosDataSourceBean.setMaxLifetime(3600); 
        return atomikosDataSourceBean;
    }

    @Bean
    public SqlSessionFactory firstSqlSessionFactory(
            @Qualifier("firstDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        return sessionFactory.getObject();
    }

}

.

@Mapper
public interface FirstMapper {

    @Select("SELECT * from first WHERE id = #{id}")
    @Results(value = {
            @Result(property = "id", column = "id"),
            @Result(property = "name", column = "name")
    })
    FirstModel selectById(@Param("id") long id);

}

.

public class FirstModel {
    private long id;
    private String name;
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

.

@RestController
public class DemoController {

    @Autowired
    FirstMapper firstMapper;

    @Autowired
    SecondMapper secondMapper;

    @GetMapping("/demo-controller")
    @Transactional
    public  String getDemoData() {

        String firstName = firstMapper.selectById(1).getName();
        String secondName = secondMapper.selectById(1).getName();

        String response = "{\"first\":"+firstName+", \"secondName\":"+secondName+"}";

        return response; 
    }

}

.

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class, MybatisAutoConfiguration.class })
@AutoConfigureMockMvc

public class DemoControllerTest {

    @MockBean
    FirstMapper firstMapper;

    @MockBean
    SecondMapper secondMapper;

    @Autowired
    MockMvc mvc;

    @Test
    public void getDemoDataTest() throws Exception {

        FirstModel firstModel = new FirstModel();
        firstModel.setId(1);
        firstModel.setName("first name");
        given(firstMapper.selectById(1l)).willReturn(firstModel);

        SecondModel secondModel = new SecondModel();
        secondModel.setId(1);
        secondModel.setName("second name");
        given(secondMapper.selectById(1l)).willReturn(secondModel);

        mvc.perform(get("/demo-controller").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
                .andExpect(jsonPath("$.first", is("first name")));

    }

}

.

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableAutoConfiguration(
        exclude = {DataSourceAutoConfiguration.class, 
                DataSourceTransactionManagerAutoConfiguration.class, 
                MybatisAutoConfiguration.class
    })

public class FirstMapperTest {
    @Autowired
    FirstMapper firstMapper;

    @Test
    public void selectByIdTest() {
        FirstModel first = firstMapper.selectById(1);
        assertEquals("first name", first.getName());
    }
}

So I ran into this and couldn't find a solution online either. Here's what I finally got to work:

I copied a lot of the AtomikosJtaConfiguration from the spring-boot-autoconfigure because the auto configuration stuff wasn't getting pulled into my tests. I found that the Spring context was getting refreshed many times while running my integration tests and on some (but not all) of the refreshes the @Bean methods were being called to recreate the beans. The datasource methods were failing because the previous datasource wasn't being cleaned up for some reason, so I decided to use a static variable to save the datasource and just return the same one every time. The rest of it just worked. One other thing you should do is set Atomikos not to write transaction logs during testing so the tests can run in parallel without stepping on each other.

 @Bean
 public DataSource dataSource() throws Exception {
   if (ATOMIKOS_DATA_SOURCE_BEAN == null) {
     if (xaDataSource == null) {
       xaDataSource = createXaDataSource();
     }
     ATOMIKOS_DATA_SOURCE_BEAN = new AtomikosXADataSourceWrapper().wrapDataSource(xaDataSource);
   }
   return ATOMIKOS_DATA_SOURCE_BEAN;
 }

I have this issue too. The way I tried to fix this issue is by marking every @SpringBootTest with @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)

@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@RunWith(SpringRunner.class)
@SpringBootTest
public class AIntegrationTest {}

@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@RunWith(SpringRunner.class)
@SpringBootTest
public class BIntegrationTest {}

Use @DirtiesContext to force Spring refresh its ApplicationConext for every @SpringBootTest class. it is slower but for me is ok because these are integration tests. Hope it helps.

I know this is an old question, but I also ran into this problem.

In the end, I went with the method described by Jason in his answer ( https://stackoverflow.com/a/49742591/5143658 ).

I created a @TestConfiguration for this, so this custom singleton datasource only exists in the TestContext. For the sake of brevity, I used XADataSourceAutoConfiguration to create the dataSource more easily.

Here's the TestConfig:

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration;
import org.springframework.boot.jdbc.XADataSourceWrapper;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;
import javax.sql.XADataSource;

@TestConfiguration
public class TestConfig {

    private static DataSource ATOMIKOS_DATASOURCE;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties,
                                 XADataSourceWrapper wrapper,
                                 ObjectProvider<XADataSource> xaDataSource) throws Exception {
        if (ATOMIKOS_DATASOURCE == null) {
            ATOMIKOS_DATASOURCE = new XADataSourceAutoConfiguration().dataSource(wrapper, dataSourceProperties, xaDataSource);
        }
        return ATOMIKOS_DATASOURCE;
    }

}

You can use this TestConfig in a test like this:

@SpringBootTest
@ContextConfiguration(classes = TestConfig.class)
public class DummyTest {

    public void testSomething(){
        // some test code
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM