SpringBootTest,Testcontainers,容器启动-映射端口只能在容器启动后获得

问题描述

我正在使用docker / testcontainers运行一个postgresql数据库进行测试。对于仅测试数据库访问的单元测试,我已经有效地做到了。但是,我现在已经将springboot测试纳入其中,以便可以使用嵌入式Web服务进行测试,但是遇到了问题。

问题似乎是在容器启动之前请求了dataSource 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");
        }

    }

这是测试中引用的我的spring boot应用程序:

@SpringBootApplication
public class TestApplication
    {

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

这里是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 can only be obtained after the container is started异常的原因是,现在在测试期间使用@SpringBootTest创建Spring Context时,它将尝试在应用程序启动时连接到数据库。

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

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

这可以确保首先启动数据库容器,等待其启动并运行,然后再启动实际测试并创建Spring Context。

我已经总结了使用Testcontainers和Spring Boot进行JUnit 4/5所需的设置机制,可以帮助您get the setup right

最后,这看起来可能像下面的

// 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() {
  }
 
}