diff --git a/testing-modules/pom.xml b/testing-modules/pom.xml
index 3dbb73973200..2d1d35680efb 100644
--- a/testing-modules/pom.xml
+++ b/testing-modules/pom.xml
@@ -59,6 +59,7 @@
spring-mockito
spring-testing-2
spring-testing-3
+ spring-testing-4
spring-testing
testing-assertions
test-containers
diff --git a/testing-modules/spring-testing-4/pom.xml b/testing-modules/spring-testing-4/pom.xml
new file mode 100644
index 000000000000..aa248fbaae16
--- /dev/null
+++ b/testing-modules/spring-testing-4/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+ spring-testing-4
+ 0.1-SNAPSHOT
+ spring-testing-4
+
+
+ com.baeldung
+ parent-boot-3
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-3
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ com.zaxxer
+ HikariCP
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+ com.github.seregamorph
+ spring-test-smart-context
+ 0.14
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework
+ spring-jdbc
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+
+ 17
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+ *IntTest.java
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ *IntTest.java
+
+
+
+
+
+
diff --git a/testing-modules/spring-testing-4/src/main/java/com/java/baeldung/spring/test/ArticlesController.java b/testing-modules/spring-testing-4/src/main/java/com/java/baeldung/spring/test/ArticlesController.java
new file mode 100644
index 000000000000..55997b09ab0e
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/main/java/com/java/baeldung/spring/test/ArticlesController.java
@@ -0,0 +1,16 @@
+package com.java.baeldung.spring.test;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/articles")
+public class ArticlesController {
+
+ @GetMapping("/{id}")
+ public String get(@PathVariable("id") long id) {
+ return "Content " + id;
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractIntegrationTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractIntegrationTest.java
new file mode 100644
index 000000000000..7f00f576ce6d
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractIntegrationTest.java
@@ -0,0 +1,23 @@
+package com.java.baeldung.spring.test;
+
+import com.github.seregamorph.testsmartcontext.SmartDirtiesContextTestExecutionListener;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+// Avoid putting @DirtiesContext on the integration test super class
+// @DirtiesContext
+// Use SmartDirtiesContextTestExecutionListener instead
+@ContextConfiguration(classes = {
+ SampleBeanTestConfiguration.class
+})
+@ActiveProfiles("test")
+@TestExecutionListeners(listeners = {
+ SmartDirtiesContextTestExecutionListener.class,
+}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
+@ExtendWith(SpringExtension.class)
+public abstract class AbstractIntegrationTest {
+
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractWebIntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractWebIntTest.java
new file mode 100644
index 000000000000..487457859b64
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/AbstractWebIntTest.java
@@ -0,0 +1,20 @@
+package com.java.baeldung.spring.test;
+
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@EnableAutoConfiguration
+@ContextConfiguration(classes = {
+ PostgresTestConfiguration.class,
+ DataSourceTestConfiguration.class,
+ ArticlesController.class
+})
+@TestPropertySource(properties = {
+ "parameter = value"
+})
+public abstract class AbstractWebIntTest extends AbstractIntegrationTest {
+
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/DataSourceTestConfiguration.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/DataSourceTestConfiguration.java
new file mode 100644
index 000000000000..88554c81196b
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/DataSourceTestConfiguration.java
@@ -0,0 +1,64 @@
+package com.java.baeldung.spring.test;
+
+import com.github.seregamorph.testsmartcontext.jdbc.LateInitDataSource;
+import com.zaxxer.hikari.HikariDataSource;
+import org.postgresql.Driver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class DataSourceTestConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(DataSourceTestConfiguration.class);
+
+// Avoid creating TestContainer objects which are not spring-managed beans
+// @Bean
+// public DataSource dataSource() {
+// // not a manageable bean!
+// var container = new PostgreSQLContainer("postgres:9.6");
+// container.start();
+// return createDataSource("main", container);
+// }
+
+ @Bean
+ public DataSource dataSource(PostgreSQLContainer> postgres) {
+ return createDataSource("main", postgres);
+ }
+
+ private static DataSource createDataSource(String name, PostgreSQLContainer> postgres) {
+ // todo schema migrations, test data insertion, etc.
+ if (postgres.isRunning()) {
+ // already running - create direct dataSource
+ logger.info("Eagerly initializing pool {}", name);
+ return createHikariDataSourceForContainer(name, postgres);
+ } else {
+ // initialize lazily on first getConnection
+ logger.info("Pool {} will be initialized lazily", name);
+ return new LateInitDataSource(name, () -> {
+ logger.info("Starting container for pool {}", name);
+ postgres.start();
+ return createHikariDataSourceForContainer(name, postgres);
+ });
+ }
+ }
+
+ private static HikariDataSource createHikariDataSourceForContainer(String name, PostgreSQLContainer> container) {
+ var hikariDataSource = new HikariDataSource();
+ hikariDataSource.setUsername(container.getUsername());
+ hikariDataSource.setPassword(container.getPassword());
+ hikariDataSource.setMinimumIdle(0);
+ hikariDataSource.setMaximumPoolSize(50);
+ hikariDataSource.setIdleTimeout(10000);
+ hikariDataSource.setConnectionTimeout(10000);
+ hikariDataSource.setAutoCommit(true);
+ hikariDataSource.setPoolName(name);
+ hikariDataSource.setDriverClassName(Driver.class.getName());
+ hikariDataSource.setJdbcUrl(container.getJdbcUrl());
+ return hikariDataSource;
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/PostgresTestConfiguration.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/PostgresTestConfiguration.java
new file mode 100644
index 000000000000..8ba2461e885c
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/PostgresTestConfiguration.java
@@ -0,0 +1,25 @@
+package com.java.baeldung.spring.test;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+@Configuration
+public class PostgresTestConfiguration {
+
+ private static PostgreSQLContainer> postgres;
+
+ // override destroy method to empty to avoid closing docker container
+ // bean on closing spring context
+ @Bean(destroyMethod = "")
+ public PostgreSQLContainer> postgresContainer() {
+ synchronized (PostgresTestConfiguration.class) {
+ if (postgres == null) {
+ postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14")
+ .asCompatibleSubstituteFor("postgres"));
+ }
+ return postgres;
+ }
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBean.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBean.java
new file mode 100644
index 000000000000..0e502457665f
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBean.java
@@ -0,0 +1,8 @@
+package com.java.baeldung.spring.test;
+
+public class SampleBean {
+
+ public String getValue() {
+ return "default";
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBeanTestConfiguration.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBeanTestConfiguration.java
new file mode 100644
index 000000000000..3d7e38e8bbfe
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleBeanTestConfiguration.java
@@ -0,0 +1,13 @@
+package com.java.baeldung.spring.test;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Import;
+
+@Import({
+ SampleBean.class,
+ SampleService.class
+})
+@TestConfiguration
+public class SampleBeanTestConfiguration {
+
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleService.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleService.java
new file mode 100644
index 000000000000..8e7837e1bb23
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/SampleService.java
@@ -0,0 +1,32 @@
+package com.java.baeldung.spring.test;
+
+import jakarta.annotation.PreDestroy;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class SampleService {
+
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(16);
+
+ private final SampleBean sampleBean;
+
+ public SampleService(SampleBean sampleBean) {
+ this.sampleBean = sampleBean;
+ }
+
+ public void scheduleNow(Runnable command, long periodSeconds) {
+ scheduler.scheduleAtFixedRate(command, 0L, periodSeconds, TimeUnit.SECONDS);
+ }
+
+ public String getValue() {
+ return sampleBean.getValue();
+ }
+
+ // to avoid thread leakage in test execution
+ @PreDestroy
+ public void shutdown() {
+ scheduler.shutdown();
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test1IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test1IntTest.java
new file mode 100644
index 000000000000..bc41f3663522
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test1IntTest.java
@@ -0,0 +1,45 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class Test1IntTest extends AbstractIntegrationTest {
+
+ @Autowired
+ private SampleBean rootBean;
+
+ @Test
+ public void test() {
+ System.out.println("Test1IT.test " + rootBean);
+ assertEquals("default", rootBean.getValue());
+ }
+
+ @Nested
+ public class NestedIT {
+
+ @Autowired
+ private SampleBean nestedBean;
+
+ @Test
+ public void nested() {
+ System.out.println("Test1IT.NestedTest.test " + nestedBean);
+ }
+
+ @Nested
+ public class DeeplyNestedIT {
+
+ @Autowired
+ private SampleService sampleService;
+
+ @Test
+ public void deeplyNested() {
+ assertEquals("default", sampleService.getValue());
+ System.out.println("Test1IT.NestedTest.DeeplyNestedTest.deeplyNested " + sampleService);
+ }
+ }
+ }
+
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test2IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test2IntTest.java
new file mode 100644
index 000000000000..fe0f1ed62315
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test2IntTest.java
@@ -0,0 +1,18 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class Test2IntTest extends AbstractIntegrationTest {
+
+ @Autowired
+ private SampleBean rootBean;
+
+ @Test
+ public void test() {
+ System.out.println("Test2IT.test " + rootBean);
+ assertEquals("default", rootBean.getValue());
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test3IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test3IntTest.java
new file mode 100644
index 000000000000..f0296a973fd2
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test3IntTest.java
@@ -0,0 +1,29 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+public class Test3IntTest extends AbstractIntegrationTest {
+
+ @Autowired
+ private SampleService sampleService;
+
+ // declaring MockBean leads to a separate spring context for this test class
+ @MockBean
+ private SampleBean sampleBean;
+
+ @BeforeEach
+ public void setUp() {
+ when(sampleBean.getValue()).thenReturn("mock");
+ }
+
+ @Test
+ public void test() {
+ assertEquals("mock", sampleBean.getValue());
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test4IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test4IntTest.java
new file mode 100644
index 000000000000..cb2f4b6607aa
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test4IntTest.java
@@ -0,0 +1,42 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
+@ContextConfiguration(classes = {
+ Test4IntTest.Configuration.class
+})
+@TestPropertySource(properties = {
+ "parameter = value"
+})
+public class Test4IntTest extends AbstractIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ public void test404() throws Exception {
+ mockMvc.perform(get("/article"))
+ .andExpect(status().isNotFound());
+ }
+
+ public static class Configuration {
+
+ @Bean
+ public MockMvc mockMvc(WebApplicationContext webApplicationContext) {
+ var builder = MockMvcBuilders.webAppContextSetup(webApplicationContext);
+ return builder.build();
+ }
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test5IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test5IntTest.java
new file mode 100644
index 000000000000..46b83918afe3
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test5IntTest.java
@@ -0,0 +1,31 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Test5IntTest extends AbstractWebIntTest {
+
+ // will inject actual dynamic port
+ @LocalServerPort
+ private int port;
+
+ @Autowired
+ private TestRestTemplate testRestTemplate;
+
+ @Test
+ public void shouldInjectLocalServerPort() {
+ assertTrue(port > 0, "port is not initialized");
+ }
+
+ @Test
+ public void testGetArticleReturns200() {
+ var entity = testRestTemplate.getForEntity("/articles/1", String.class);
+ assertEquals(200, entity.getStatusCode().value());
+ assertEquals("Content 1", entity.getBody());
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test6IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test6IntTest.java
new file mode 100644
index 000000000000..29f8c8d7622c
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test6IntTest.java
@@ -0,0 +1,19 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class Test6IntTest extends AbstractWebIntTest {
+
+ @Autowired
+ private TestRestTemplate testRestTemplate;
+
+ @Test
+ public void testGetArticlesReturns404() {
+ var entity = testRestTemplate.getForEntity("/articles", String.class);
+ assertEquals(404, entity.getStatusCode().value());
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test7IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test7IntTest.java
new file mode 100644
index 000000000000..2aa9d5904de3
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test7IntTest.java
@@ -0,0 +1,21 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+public class Test7IntTest extends AbstractIntegrationTest {
+
+ @Test
+ public void test() {
+ System.out.println("Test7IT.test");
+ }
+
+ @Nested
+ public class NestedIT {
+
+ @Test
+ public void nested() {
+ System.out.println("Test7IT.NestedTest.test");
+ }
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test8IntTest.java b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test8IntTest.java
new file mode 100644
index 000000000000..bc02344573ca
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/java/com/java/baeldung/spring/test/Test8IntTest.java
@@ -0,0 +1,19 @@
+package com.java.baeldung.spring.test;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class Test8IntTest extends AbstractWebIntTest {
+
+ @Autowired
+ private TestRestTemplate testRestTemplate;
+
+ @Test
+ public void testPostArticlesReturns405() {
+ var entity = testRestTemplate.postForEntity("/articles/1", new byte[0], String.class);
+ assertEquals(405, entity.getStatusCode().value());
+ }
+}
diff --git a/testing-modules/spring-testing-4/src/test/resources/spring.properties b/testing-modules/spring-testing-4/src/test/resources/spring.properties
new file mode 100644
index 000000000000..7285e78ee762
--- /dev/null
+++ b/testing-modules/spring-testing-4/src/test/resources/spring.properties
@@ -0,0 +1,2 @@
+# limit the spring-test context cache max size (default 32)
+spring.test.context.cache.maxSize=4