BAEL-6097: Pitfalls on Testing with Spring Boot (#13441)
* BAEL-6097: Create project * BAEL-6097: Implement sample code and tests with custom test slices * BAEL-6097: Fix application-test.yml * BAEL-6097: Rename tests to match BDD naming strategy, add test for Mapper Integration Test
This commit is contained in:
parent
c311ca5ad3
commit
e4214237da
4
pom.xml
4
pom.xml
|
@ -965,6 +965,7 @@
|
|||
<module>spring-boot-modules/spring-boot-3</module>
|
||||
<module>spring-boot-modules/spring-boot-3-native</module>
|
||||
<module>spring-boot-modules/spring-boot-3-observation</module>
|
||||
<module>spring-boot-modules/spring-boot-3-test-pitfalls</module>
|
||||
<module>spring-swagger-codegen/custom-validations-opeanpi-codegen</module>
|
||||
<module>testing-modules/testing-assertions</module>
|
||||
<module>persistence-modules/fauna</module>
|
||||
|
@ -1163,6 +1164,7 @@
|
|||
<module>spring-boot-modules/spring-boot-3</module>
|
||||
<module>spring-boot-modules/spring-boot-3-native</module>
|
||||
<module>spring-boot-modules/spring-boot-3-observation</module>
|
||||
<module>spring-boot-modules/spring-boot-3-test-pitfalls</module>
|
||||
<module>spring-swagger-codegen/custom-validations-opeanpi-codegen</module>
|
||||
<module>testing-modules/testing-assertions</module>
|
||||
<module>persistence-modules/fauna</module>
|
||||
|
@ -1283,7 +1285,7 @@
|
|||
<java.version>11</java.version>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
|
||||
<profile>
|
||||
<id>parents</id>
|
||||
<modules>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-boot-3-test-pitfalls</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>spring-boot-3-test-pitfalls</name>
|
||||
<description>Demo project for Spring Boot Testing Pitfalls</description>
|
||||
|
||||
<parent>
|
||||
<groupId>com.baeldung</groupId>
|
||||
<artifactId>parent-boot-3</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>../../parent-boot-3</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
|
||||
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
|
||||
</properties>
|
||||
|
||||
|
||||
</project>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.baeldung.sample.pets.boundary;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PetDto {
|
||||
|
||||
private String name;
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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<PetDto> readAll() {
|
||||
return service.getPets().stream()
|
||||
.map(mapper::map)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package com.baeldung.sample.pets.domain;
|
||||
|
||||
public record Pet(String name) {
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.baeldung.sample.pets.domain;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface PetServiceRepository {
|
||||
|
||||
boolean add(Pet pet);
|
||||
|
||||
void clear();
|
||||
|
||||
Collection<Pet> getPets();
|
||||
|
||||
}
|
|
@ -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<Pet> pets = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public Set<Pet> getPets() {
|
||||
return Collections.unmodifiableSet(pets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Pet pet) {
|
||||
return this.pets.add(pet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
this.pets.clear();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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("[]"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
logging:
|
||||
level:
|
||||
root: info
|
||||
org:
|
||||
springframework:
|
||||
test:
|
||||
context:
|
||||
cache: DEBUG
|
||||
spring:
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
Loading…
Reference in New Issue