diff --git a/pom.xml b/pom.xml
index 4016d669a4..83ba73229f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -965,6 +965,7 @@
spring-boot-modules/spring-boot-3
spring-boot-modules/spring-boot-3-native
spring-boot-modules/spring-boot-3-observation
+ spring-boot-modules/spring-boot-3-test-pitfalls
spring-swagger-codegen/custom-validations-opeanpi-codegen
testing-modules/testing-assertions
persistence-modules/fauna
@@ -1163,6 +1164,7 @@
spring-boot-modules/spring-boot-3
spring-boot-modules/spring-boot-3-native
spring-boot-modules/spring-boot-3-observation
+ spring-boot-modules/spring-boot-3-test-pitfalls
spring-swagger-codegen/custom-validations-opeanpi-codegen
testing-modules/testing-assertions
persistence-modules/fauna
@@ -1283,7 +1285,7 @@
11
-
+
parents
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/pom.xml b/spring-boot-modules/spring-boot-3-test-pitfalls/pom.xml
new file mode 100644
index 0000000000..90e4ba022a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/pom.xml
@@ -0,0 +1,87 @@
+
+
+ 4.0.0
+ spring-boot-3-test-pitfalls
+ 0.0.1-SNAPSHOT
+ spring-boot-3-test-pitfalls
+ Demo project for Spring Boot Testing Pitfalls
+
+
+ com.baeldung
+ parent-boot-3
+ 0.0.1-SNAPSHOT
+ ../../parent-boot-3
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.mapstruct
+ mapstruct
+ ${org.mapstruct.version}
+ true
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${org.mapstruct.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
+
+
+
+
+
+ 1.5.3.Final
+ 3.0.0-M7
+
+
+
+
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/PetsApplication.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/PetsApplication.java
new file mode 100644
index 0000000000..17ba1abb2e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/PetsApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.sample.pets;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class PetsApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PetsApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDto.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDto.java
new file mode 100644
index 0000000000..62b1202ce8
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDto.java
@@ -0,0 +1,10 @@
+package com.baeldung.sample.pets.boundary;
+
+import lombok.Data;
+
+@Data
+public class PetDto {
+
+ private String name;
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDtoMapper.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDtoMapper.java
new file mode 100644
index 0000000000..07fbb77f2e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetDtoMapper.java
@@ -0,0 +1,13 @@
+package com.baeldung.sample.pets.boundary;
+
+import com.baeldung.sample.pets.domain.Pet;
+import org.mapstruct.Mapper;
+
+@Mapper(componentModel = "spring")
+public interface PetDtoMapper {
+
+ PetDto map(Pet source);
+
+ Pet map(PetDto source);
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetsController.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetsController.java
new file mode 100644
index 0000000000..d95d43028b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/boundary/PetsController.java
@@ -0,0 +1,28 @@
+package com.baeldung.sample.pets.boundary;
+
+import com.baeldung.sample.pets.domain.PetService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/pets")
+@RequiredArgsConstructor
+public class PetsController {
+
+ private final PetService service;
+ private final PetDtoMapper mapper;
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public Collection readAll() {
+ return service.getPets().stream()
+ .map(mapper::map)
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/Pet.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/Pet.java
new file mode 100644
index 0000000000..3dfe5e1a47
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/Pet.java
@@ -0,0 +1,4 @@
+package com.baeldung.sample.pets.domain;
+
+public record Pet(String name) {
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetService.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetService.java
new file mode 100644
index 0000000000..c5f83047b8
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetService.java
@@ -0,0 +1,14 @@
+package com.baeldung.sample.pets.domain;
+
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class PetService {
+
+ @Delegate
+ private final PetServiceRepository repo;
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepository.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepository.java
new file mode 100644
index 0000000000..1900f72a1c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepository.java
@@ -0,0 +1,13 @@
+package com.baeldung.sample.pets.domain;
+
+import java.util.Collection;
+
+public interface PetServiceRepository {
+
+ boolean add(Pet pet);
+
+ void clear();
+
+ Collection getPets();
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepositoryImpl.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepositoryImpl.java
new file mode 100644
index 0000000000..d1d4bba175
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/java/com/baeldung/sample/pets/domain/PetServiceRepositoryImpl.java
@@ -0,0 +1,29 @@
+package com.baeldung.sample.pets.domain;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@Component
+public class PetServiceRepositoryImpl implements PetServiceRepository {
+
+ private final Set pets = new HashSet<>();
+
+ @Override
+ public Set getPets() {
+ return Collections.unmodifiableSet(pets);
+ }
+
+ @Override
+ public boolean add(Pet pet) {
+ return this.pets.add(pet);
+ }
+
+ @Override
+ public void clear() {
+ this.pets.clear();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/resources/application.yml b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/resources/application.yml
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/main/resources/application.yml
@@ -0,0 +1 @@
+
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetDtoMapperIntegrationTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetDtoMapperIntegrationTest.java
new file mode 100644
index 0000000000..14b12cb6e9
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetDtoMapperIntegrationTest.java
@@ -0,0 +1,47 @@
+package com.baeldung.sample.pets.boundary;
+
+import com.baeldung.sample.pets.domain.PetService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.MockReset;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(SpringExtension.class)
+public class PetDtoMapperIntegrationTest {
+
+ @Configuration
+ @ComponentScan(basePackageClasses = PetDtoMapper.class)
+ static class PetDtoMapperTestConfig {
+
+ /*
+ * This would be necessary because the controller is also initialized
+ * and needs the service, although we do not want to test it here.
+ *
+ * Solutions:
+ * - place the mapper into a separate sub package
+ * - do not test the mapper separately, test it integrated within the controller
+ * (recommended)
+ */
+ @Bean
+ PetService createServiceMock() {
+ return mock(PetService.class, MockReset.withSettings(MockReset.AFTER));
+ }
+
+ }
+
+ @Autowired
+ PetDtoMapper mapper;
+
+ @Test
+ void shouldExist() { // simply test correct test setup
+ assertThat(mapper).isNotNull();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsBoundaryLayer.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsBoundaryLayer.java
new file mode 100644
index 0000000000..2a83b364c3
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsBoundaryLayer.java
@@ -0,0 +1,10 @@
+package com.baeldung.sample.pets.boundary;
+
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * Just an interface to use for compiler-checked component scanning during tests.
+ * @see ComponentScan#basePackageClasses()
+ */
+public interface PetsBoundaryLayer {
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsControllerMvcIntegrationTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsControllerMvcIntegrationTest.java
new file mode 100644
index 0000000000..9a5df7b727
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/boundary/PetsControllerMvcIntegrationTest.java
@@ -0,0 +1,36 @@
+package com.baeldung.sample.pets.boundary;
+
+import com.baeldung.sample.pets.domain.PetService;
+import com.baeldung.sample.test.slices.PetsBoundaryTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Collections;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@PetsBoundaryTest
+class PetsControllerMvcIntegrationTest {
+
+ @Autowired
+ MockMvc mvc;
+ @Autowired
+ PetService service;
+
+ @Test
+ void shouldReturnEmptyArrayWhenGetPets() throws Exception {
+ when(service.getPets()).thenReturn(Collections.emptyList());
+ mvc.perform(
+ get("/pets")
+ .accept(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isOk())
+ .andExpect(content().string("[]"));
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceIntegrationTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceIntegrationTest.java
new file mode 100644
index 0000000000..5e2ec41089
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceIntegrationTest.java
@@ -0,0 +1,26 @@
+package com.baeldung.sample.pets.domain;
+
+import com.baeldung.sample.test.slices.PetsDomainTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@PetsDomainTest
+class PetServiceIntegrationTest {
+
+ @Autowired
+ PetService service;
+ @Autowired // Mock
+ PetServiceRepository repository;
+
+ @Test
+ void shouldAddPetWhenNotAlreadyExisting() {
+ var pet = new Pet("Dog");
+ when(repository.add(pet)).thenReturn(true);
+ var result = service.add(pet);
+ assertThat(result).isTrue();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceUnitTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceUnitTest.java
new file mode 100644
index 0000000000..b0ecdfaeb7
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetServiceUnitTest.java
@@ -0,0 +1,31 @@
+package com.baeldung.sample.pets.domain;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PetServiceUnitTest {
+
+ PetService service = new PetService(new PetServiceRepositoryImpl());
+
+ @Test
+ void shouldAddPetWhenNotAlreadyExisting() {
+ var pet = new Pet("Dog");
+ var result = service.add(pet);
+ assertThat(result).isTrue();
+ assertThat(service.getPets()).hasSize(1);
+ }
+
+ @Test
+ void shouldNotAddPetWhenAlreadyExisting() {
+ var pet = new Pet("Cat");
+ var result = service.add(pet);
+ assertThat(result).isTrue();
+ // try a second time
+ result = service.add(pet);
+ assertThat(result).isFalse();
+ assertThat(service.getPets()).hasSize(1);
+ }
+
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetsDomainLayer.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetsDomainLayer.java
new file mode 100644
index 0000000000..f32fd189e0
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/pets/domain/PetsDomainLayer.java
@@ -0,0 +1,10 @@
+package com.baeldung.sample.pets.domain;
+
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * Just an interface to use for compiler-checked component scanning during tests.
+ * @see ComponentScan#basePackageClasses()
+ */
+public interface PetsDomainLayer {
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsBoundaryTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsBoundaryTest.java
new file mode 100644
index 0000000000..f58239f971
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsBoundaryTest.java
@@ -0,0 +1,52 @@
+package com.baeldung.sample.test.slices;
+
+import com.baeldung.sample.pets.boundary.PetsBoundaryLayer;
+import com.baeldung.sample.pets.boundary.PetsController;
+import com.baeldung.sample.pets.domain.PetService;
+import org.junit.jupiter.api.Tag;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.mock.mockito.MockReset;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.mockito.Mockito.mock;
+
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@WebMvcTest(controllers = PetsController.class)
+@ComponentScan(basePackageClasses = PetsBoundaryLayer.class)
+@Import(PetsBoundaryTest.PetBoundaryTestConfiguration.class)
+// further features that can help to configure and execute tests
+@ActiveProfiles({ "test", "boundary-test" })
+@Tag("integration-test")
+@Tag("boundary-test")
+public @interface PetsBoundaryTest {
+
+ @TestConfiguration
+ class PetBoundaryTestConfiguration {
+
+ @Primary
+ @Bean
+ PetService createPetServiceMock() {
+ return mock(
+ PetService.class,
+ MockReset.withSettings(MockReset.AFTER)
+ );
+ }
+
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsDomainTest.java b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsDomainTest.java
new file mode 100644
index 0000000000..b061889135
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/java/com/baeldung/sample/test/slices/PetsDomainTest.java
@@ -0,0 +1,52 @@
+package com.baeldung.sample.test.slices;
+
+import com.baeldung.sample.pets.domain.PetServiceRepository;
+import com.baeldung.sample.pets.domain.PetsDomainLayer;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.test.mock.mockito.MockReset;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.mockito.Mockito.mock;
+
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(SpringExtension.class)
+@ComponentScan(basePackageClasses = PetsDomainLayer.class)
+@Import(PetsDomainTest.PetServiceTestConfiguration.class)
+// further features that can help to configure and execute tests
+@ActiveProfiles({"test", "domain-test"})
+@Tag("integration-test")
+@Tag("domain-test")
+public @interface PetsDomainTest {
+
+ @TestConfiguration
+ class PetServiceTestConfiguration {
+
+ @Primary
+ @Bean
+ PetServiceRepository createPetsRepositoryMock() {
+ return mock(
+ PetServiceRepository.class,
+ MockReset.withSettings(MockReset.AFTER)
+ );
+ }
+
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/resources/application-test.yml b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/resources/application-test.yml
new file mode 100644
index 0000000000..9801fe9e7e
--- /dev/null
+++ b/spring-boot-modules/spring-boot-3-test-pitfalls/src/test/resources/application-test.yml
@@ -0,0 +1,11 @@
+logging:
+ level:
+ root: info
+ org:
+ springframework:
+ test:
+ context:
+ cache: DEBUG
+spring:
+ main:
+ allow-bean-definition-overriding: true