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:
Ralf Ueberfuhr 2023-02-14 07:28:13 +01:00 committed by GitHub
parent c311ca5ad3
commit e4214237da
20 changed files with 490 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
package com.baeldung.sample.pets.boundary;
import lombok.Data;
@Data
public class PetDto {
private String name;
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package com.baeldung.sample.pets.domain;
public record Pet(String name) {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("[]"));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
logging:
level:
root: info
org:
springframework:
test:
context:
cache: DEBUG
spring:
main:
allow-bean-definition-overriding: true