springcorn
springcorn

Reputation: 631

SpringBootTest, Testcontainers, container start up - Mapped port can only be obtained after the container is started

I am using docker/testcontainers to run a postgresql db for testing. I have effectively done this for unit testing that is just testing the database access. However, I have now brought springboot testing into the mix so I can test with an embedded web service and I am having problems.

The issue seems to be that the dataSource bean is being requested before the container starts.

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

Here is my 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");
        }

    }

Here is my spring boot application referenced from the test:

@SpringBootApplication
public class TestApplication
    {

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

Here is the IntegrationDataService class which is intended to startup the container and provide the sessionfactory/datasource for everything else

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

    }

The error occurs on requesting the datasource bean from the sessionFactory from one of the Dao classes before the container starts up.

What the heck am I doing wrong?

Thanks!!!

Upvotes: 5

Views: 11848

Answers (1)

rieckpil
rieckpil

Reputation: 12021

The reason for your java.lang.IllegalStateException: Mapped port can only be obtained after the container is started exception is that when the Spring Context now gets created during your test with @SpringBootTest it tries to connect to the database on application startup.

As you only launch your PostgreSQL inside your IntegrationDataService class, there is a timing issue as you can't obtain the JDBC URL or create a connection on application startup as this bean is not yet properly created.

In general, you should NOT use any test-related code inside your IntegrationDataService class. Starting/stopping the database should be done inside your test setup.

This ensures to first start the database container, wait until it's up- and running, and only then launch the actual test and create the Spring Context.

I've summarized the required setup mechanism for JUnit 4/5 with Testcontainers and Spring Boot, that help you get the setup right.

In the end, this can look like the following

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

Upvotes: 13

Related Questions