Spring boot SpringBootTest、Testcontainers、容器启动映射端口只能在容器启动后获得

Spring boot SpringBootTest、Testcontainers、容器启动映射端口只能在容器启动后获得,spring-boot,junit,testcontainers,Spring Boot,Junit,Testcontainers,我使用docker/testcontainers运行postgresql数据库进行测试。我已经为单元测试有效地做到了这一点,单元测试只是测试数据库访问。然而,我现在已经将springboot测试引入到混合测试中,这样我就可以使用嵌入式web服务进行测试,我遇到了一些问题 问题似乎是在容器启动之前请求数据源bean Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with

我使用docker/testcontainers运行postgresql数据库进行测试。我已经为单元测试有效地做到了这一点,单元测试只是测试数据库访问。然而,我现在已经将springboot测试引入到混合测试中,这样我就可以使用嵌入式web服务进行测试,我遇到了一些问题

问题似乎是在容器启动之前请求数据源bean

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [com/myproject/integrationtests/IntegrationDataService.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.sql.DataSource]: Factory method 'dataSource' threw exception; nested exception is java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.sql.DataSource]: Factory method 'dataSource' threw exception; nested exception is java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
Caused by: java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
以下是我的SpringBootTest:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {IntegrationDataService.class,  TestApplication.class},
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootTestControllerTesterIT
    {
    @Autowired
    private MyController myController;
    @LocalServerPort
    private int port;
    @Autowired
    private TestRestTemplate restTemplate;


    @Test
    public void testRestControllerHello()
        {
        String url = "http://localhost:" + port + "/mycontroller/hello";
        ResponseEntity<String> result =
                restTemplate.getForEntity(url, String.class);
        assertEquals(result.getStatusCode(), HttpStatus.OK);
        assertEquals(result.getBody(), "hello");
        }

    }
下面是IntegrationDataService类,用于启动容器并为其他所有内容提供sessionfactory/datasource

@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@EnableTransactionManagement
@Configuration
public  class IntegrationDataService
    {
    @Container
    public static PostgreSQLContainer postgreSQLContainer = (PostgreSQLContainer) new PostgreSQLContainer("postgres:9.6")
            .withDatabaseName("test")
            .withUsername("sa")
            .withPassword("sa")
            .withInitScript("db/postgresql/schema.sql");   

    @Bean
    public Properties hibernateProperties()
        {
        Properties hibernateProp = new Properties();
        hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        hibernateProp.put("hibernate.format_sql", true);
        hibernateProp.put("hibernate.use_sql_comments", true);
//        hibernateProp.put("hibernate.show_sql", true);
        hibernateProp.put("hibernate.max_fetch_depth", 3);
        hibernateProp.put("hibernate.jdbc.batch_size", 10);
        hibernateProp.put("hibernate.jdbc.fetch_size", 50);
        hibernateProp.put("hibernate.id.new_generator_mappings", false);
//        hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
//        hibernateProp.put("hibernate.jdbc.lob.non_contextual_creation", true);
        return hibernateProp;
        }

    @Bean
    public SessionFactory sessionFactory() throws IOException
        {
        LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource());
        sessionFactoryBean.setHibernateProperties(hibernateProperties());
        sessionFactoryBean.setPackagesToScan("com.myproject.model.entities");
        sessionFactoryBean.afterPropertiesSet();
        return sessionFactoryBean.getObject();
        }

    @Bean
    public PlatformTransactionManager transactionManager() throws IOException
        {
        return new HibernateTransactionManager(sessionFactory());
        }     
   
    @Bean
    public DataSource dataSource()
        {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(postgreSQLContainer.getDriverClassName());
        dataSource.setUrl(postgreSQLContainer.getJdbcUrl());
        dataSource.setUsername(postgreSQLContainer.getUsername());
        dataSource.setPassword(postgreSQLContainer.getPassword());
        return dataSource;
        }

    }
在容器启动之前,从一个Dao类从sessionFactory请求数据源bean时会发生错误

我到底做错了什么


谢谢

您的
java.lang.IllegalStateException:Mapped port只能在容器启动后获得
异常的原因是,当您在使用
@SpringBootTest
测试期间创建Spring上下文时,它会在应用程序启动时尝试连接到数据库

由于您仅在
IntegrationDataService
类中启动PostgreSQL,因此存在一个时间问题,因为您无法在应用程序启动时获取JDBC URL或创建连接,因为此bean尚未正确创建

通常,您应该不要在
IntegrationDataService
类中使用任何与测试相关的代码。启动/停止数据库应该在测试设置中完成

这确保首先启动数据库容器,等待它启动并运行,然后才启动实际测试并创建Spring上下文

我总结了使用Testcontainers和Spring Boot的JUnit 4/5所需的设置机制,这对您有所帮助

最后,它可以如下所示

// JUnit 5 example with Spring Boot >= 2.2.6
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {
 
  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername("inmemory");
 
  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }
 
  @Test
  public void contextLoads() {
  }
 
}

令人惊叹的。非常感谢。我让它工作了。但问题是,让IntegrationDataService类ApplicationContextAware访问您注册的这些属性的最佳方法是什么?(这就是我所做的)如果不是严格必要的话,我不会自己手动创建Hibernate
会话。让Spring Boot为您的应用程序自动配置一切,而您只需在
应用程序中提供
Spring.datasource.url
等。yml
此设置更简单。谢谢你的回答。
// JUnit 5 example with Spring Boot >= 2.2.6
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationIT {
 
  @Container
  public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
    .withPassword("inmemory")
    .withUsername("inmemory");
 
  @DynamicPropertySource
  static void postgresqlProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
    registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
    registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
  }
 
  @Test
  public void contextLoads() {
  }
 
}