5

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!!!

1 Answer 1

13

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() { } } 
Sign up to request clarification or add additional context in comments.

2 Comments

Awesome. Thank you. I got it working. Question though, is the best way to then access these properties you registered by making the IntegrationDataService class ApplicationContextAware? (that is what I did)
I wouldn't do any manual Hibernate Session creation on my own if not strictly necessary. Let Spring Boot auto-configure everything for your while you just provide spring.datasource.url etc. in your application.yml

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.