如何构建 Spring Boot Atomikos 测试配置?

Pav*_*vlo 5 java testing spring atomikos spring-boot

如何创建适当的测试环境,以便能够在同一应用程序中使用数据库层测试和带有模拟的 REST 端点测试?

我有一个带有两个数据源的 Spring Boot 应用程序。用于管理 Atomikos 使用的交易。这个配置工作正常。

现在我需要创建测试。我构建了一个测试配置,每个测试都工作正常,但当我运行所有测试时它都会失败。在我看来(参见堆栈跟踪),问题是如果实例化很少的 Atomikos beans,Atomikos 就无法工作。

我尝试了两种解决方案来使 Atomikos beans 仅实例化一次:

  1. 创建一个用于所有测试的测试配置(因为 Spring 缓存测试上下文)。但这不起作用。我认为这是因为 @Controller 中的 Mock beans 破坏了重用 Spring 测试上下文的能力。我在测试中使用的持久性映射器组件在一个测试中被模拟,同时在其他测试中使用真实实例。所以我看到每个测试类都在它自己的测试上下文中运行。

  2. 在数据库 @Configuration 类上使用 @Lazy 注释。我认为这将确保 bean 仅在第一次调用时才会被实例化,并将在以后的调用中重用。但这也行不通。

这是我用来说明问题的示例项目链接。该存储库包括 MySQL 数据库转储: https: //github.com/pavelmorozov/AtomikosConfig

在这篇文章中,我将仅展示两个数据库模型、映射器和配置类中的一个,因为它们对于第二个数据库几乎相同。

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
Run Code Online (Sandbox Code Playgroud)

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

    public static void main(String[] args) {
        SpringApplication.run(AtomikosConfigApplication.class, args);
    }
}
Run Code Online (Sandbox Code Playgroud)

@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();
    }

}
Run Code Online (Sandbox Code Playgroud)

@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);

}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

@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; 
    }

}
Run Code Online (Sandbox Code Playgroud)

@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")));

    }

}
Run Code Online (Sandbox Code Playgroud)

@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());
    }
}
Run Code Online (Sandbox Code Playgroud)

Jas*_*ira 2

所以我遇到了这个问题,在网上也找不到解决方案。这是我终于开始工作的:

我从 spring-boot-autoconfigure 复制了很多 AtomikosJtaConfiguration ,因为自动配置内容没有被纳入我的测试中。我发现在运行集成测试时,Spring 上下文被刷新了很多次,并且在某些(但不是全部)刷新中,@Bean 方法被调用来重新创建 bean。数据源方法失败,因为以前的数据源由于某种原因没有被清理,所以我决定使用静态变量来保存数据源,并且每次都返回相同的数据源。剩下的部分就起作用了。您应该做的另一件事是设置 Atomikos 在测试期间不写入事务日志,以便测试可以并行运行而不会相互干扰。

 @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;
 }
Run Code Online (Sandbox Code Playgroud)

  • 仅为了解决此问题而在生产代码中进行这些更改并不是好的做法。它为单元测试添加了不必要的耦合,并且 Atomikos 的这种特定行为仅出现在 Spring 集成测试中。 (3认同)