BAEL-5630: Spring Boot 3 Sample (#12449)

* BAEL-5630: Spring Boot 3 Sample

* BAEL-5630: Reorganize project in Maven profiles

* BAEL-5630: Upgrade Maven PMD plugin version

* BAEL-5630: Rename tests to follow naming conventions
This commit is contained in:
Ralf Ueberfuhr
2022-07-28 21:04:26 +02:00
committed by GitHub
parent c0d5df745e
commit 33c18f2cd5
28 changed files with 1240 additions and 0 deletions
@@ -0,0 +1,13 @@
package com.baeldung.sample.boundary;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.ComponentScan;
/**
* This class bundles all classes in the boundary layer.
* This includes also the generated mapper classes.
*/
@TestConfiguration
@ComponentScan(basePackageClasses = TodosBoundaryLayer.class)
public class TodosBoundaryLayer {
}
@@ -0,0 +1,218 @@
package com.baeldung.sample.boundary;
import com.baeldung.sample.control.NotFoundException;
import com.baeldung.sample.control.Todo;
import com.baeldung.sample.control.TodosService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integrationstests der Todos REST API auf HTTP Layer. Die Anwendung wird als Black Box behandelt.
* Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen.
* Spring Boot startet den sog. "ApplicationContext" automatisch und bietet Möglichkeiten, die Anwendungsteile vom Test aus aufzurufen.
*/
@WebMvcTest
@ContextConfiguration(classes = TodosBoundaryLayer.class)
class TodosControllerApiIntegrationTest {
private static final String BASEURL = "/api/v1/todos"; // URL to Resource
private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_VALUE;
@MockBean
TodosService service;
@Autowired
MockMvc mvc; // testing by sending HTTP requests and verifying HTTP responses
@Autowired
ObjectMapper mapper; // used to render or parse JSON
/*
* Testfall:
* - GET auf alle Todos -> 200 OK mit JSON
*/
@DisplayName("GET auf alle Daten (200 OK)")
@Test
void testFindAllTodos() throws Exception {
when(service.findAll()).thenReturn(List.of());
mvc
.perform(get(BASEURL).accept(DEFAULT_MEDIA_TYPE))
.andExpect(status().isOk())
.andExpect(content().contentType(DEFAULT_MEDIA_TYPE));
}
/*
* Testfall:
* - einzelnes Todos auslesen -> kein Fehler
*/
@Test
void testFindById() throws Exception {
when(service.findById(1L))
.thenReturn(Optional.of(new Todo(1L, "test")));
mvc
.perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE))
.andExpect(status().isOk())
.andExpect(content().contentType(DEFAULT_MEDIA_TYPE))
.andExpect(jsonPath("$.title").value("test"));
}
/*
* Testfall:
* - einzelnes Todos auslesen -> 404
*/
@Test
void testFindByIdNotExisting() throws Exception {
when(service.findById(1L))
.thenReturn(Optional.empty());
mvc
.perform(get(BASEURL + "/1").accept(DEFAULT_MEDIA_TYPE))
.andExpect(status().isNotFound());
}
/*
* Testfall:
* - Anlegen eines Todos per POST -> 201 mit Location Header
*/
@DisplayName("POST liefert 201 mit Location-Header")
@Test
void testCreateTodo() throws Exception {
// etwas umständlich, die ID zu besetzen, wenn auf dem Service create() aufgerufen wird
when(service.create(any())).thenReturn(new Todo(5L, "test-todo"));
final var json = "{\"title\":\"test-todo\"}";
mvc
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isCreated())
.andExpect(header().exists(HttpHeaders.LOCATION))
.andExpect(jsonPath("$.id").value(5L));
}
/*
* Testfall:
* - Anlegen eines Todos per POST ohne Titel -> 422
*/
@DisplayName("POST erzeugt kein Todo, wenn kein Titel angegeben ist")
@Test
void testCreateTodoWithoutTitle() throws Exception {
final var json = "{}";
mvc
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isUnprocessableEntity());
verifyNoInteractions(service);
}
/*
* Testfall:
* - Anlegen eines Todos per POST mit ID -> 400
*/
@DisplayName("POST erzeugt kein Todo, wenn die ID mitgegeben wird (undefinierte Property)")
@Test
void testCreateTodoWithID() throws Exception {
final var json = "{\"id\":1, \"title\":\"test\"}";
mvc
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isBadRequest());
verifyNoInteractions(service);
}
/*
* Testfall:
* - Anlegen eines Todos per POST mit leerem Titel -> 400
*/
@DisplayName("POST erzeugt kein Todo, wenn Titel weniger als 1 Zeichen hat")
@Test
void testCreateTodoWithEmptyTitle() throws Exception {
final var newTodo = new TodoRequestDto();
newTodo.setTitle("");
final String json = mapper.writeValueAsString(newTodo);
this.mvc //
.perform(post(BASEURL).contentType(DEFAULT_MEDIA_TYPE).content(json)) //
.andExpect(status().isUnprocessableEntity());
}
/*
* Testfall:
* - Ändern -> 204
*/
@Test
void testUpdateTodo() throws Exception {
final var json = "{\"title\":\"test-todo\"}";
mvc
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isNoContent());
}
/*
* Testfall:
* - Ändern -> 404
*/
@Test
void testUpdateTodoNotExisting() throws Exception {
doThrow(NotFoundException.class).when(service).update(any());
final var json = "{\"title\":\"test-todo\"}";
mvc
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isNotFound());
}
/*
* Testfall:
* - Ändern des Todos mit leerem Titel
*/
@Test
@DisplayName("PUT mit leerem Titel")
void testUpdateWithEmptyTitle() throws Exception {
// Act
final var json = "{}";
mvc
.perform(put(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE).content(json))
.andExpect(status().isUnprocessableEntity());
verifyNoInteractions(service);
}
/*
* Testfall:
* - Löschen -> 204
*/
@Test
void testDeleteTodo() throws Exception {
mvc
.perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE))
.andExpect(status().isNoContent());
}
/*
* Testfall:
* - Löschen -> 404
*/
@Test
void testDeleteTodoNotExisting() throws Exception {
doThrow(NotFoundException.class).when(service).delete(anyLong());
mvc
.perform(delete(BASEURL + "/5").contentType(DEFAULT_MEDIA_TYPE))
.andExpect(status().isNotFound());
}
}
@@ -0,0 +1,13 @@
package com.baeldung.sample.control;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.ComponentScan;
/**
* This class bundles all classes in the control layer.
* This includes also the generated mapper classes.
*/
@TestConfiguration
@ComponentScan(basePackageClasses = TodosControlLayer.class)
public class TodosControlLayer {
}
@@ -0,0 +1,39 @@
package com.baeldung.sample.control;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@SpringBootTest
class TodosInitializerActivationIntegrationTest {
@MockBean
TodosService service;
@BeforeEach
void serviceHasEmptyData() {
when(service.count()).thenReturn(0L);
}
@AfterEach
void serviceHasNoFurtherInteractions() {
verifyNoMoreInteractions(service);
}
@Test
@DisplayName("data initialization is invoked on default profile")
void testIsInvoked() {
verify(service).count();
verify(service, atLeastOnce()).create(any());
}
}
@@ -0,0 +1,63 @@
package com.baeldung.sample.control;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest
@AutoConfigureTestDatabase
@Transactional
@TestPropertySource(
properties = {
"application.data.initialize-on-startup=false"
}
)
class TodosServiceDatabaseIntegrationTest {
@Autowired
TodosService service;
@BeforeEach
void assertEmpty() {
assertThat(service.count())
.isZero();
}
@Test
void testCreate() {
service.create(new Todo(null, "test"));
assertThat(service.count())
.isEqualTo(1);
assertThat(service.findAll())
.hasSize(1)
.element(0).extracting(Todo::title).isEqualTo("test");
}
@Test
void testFindById() {
final var todo = service.create(new Todo(null, "test"));
final var result = service.findById(todo.id());
assertThat(result)
.isNotEmpty()
.get().usingRecursiveComparison().isEqualTo(todo);
}
@Test
void testDelete() {
final var todo = service.create(new Todo(null, "test"));
final var id = todo.id();
service.delete(id);
final var result = service.findById(id);
assertThat(result).isEmpty();
assertThatThrownBy(() -> service.delete(id))
.isInstanceOf(NotFoundException.class);
}
}
@@ -0,0 +1,126 @@
package com.baeldung.sample.control;
import com.baeldung.sample.entity.TodoEntity;
import com.baeldung.sample.entity.TodosRepository;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
/**
* Integrationstests des TodosServices. Die Anwendung wird als Black Box behandelt.
* Somit müssen alle Tests, die einen existierenden Datensatz benötigen, diesen im Test anlegen.
*/
@SpringBootTest(classes = TodosControlLayer.class)
class TodosServiceIntegrationTest {
// dieses Objekt wird als Mock instruiert
// Answers.RETURNS_MOCKS ist notwendig, da Methoden in Service-Init-Methode bereits aufgerufen werden
@MockBean(answer = Answers.RETURNS_MOCKS)
TodosRepository repo;
@Autowired
TodosService service;
/*
* Testfall:
* - alle Todos auslesen -> kein Fehler
*/
@Test
void testFindAllTodos() {
when(repo.findAll()).thenReturn(List.of(new TodoEntity(1L, "test")));
final var result = service.findAll();
assertThat(result).hasSize(1) //
.element(0).extracting(Todo::id, Todo::title).containsExactly(1L, "test");
}
/*
* Testfall:
* - Anlegen eines Todos -> ID besetzt
* - Auslesen -> gefunden mit entsprechenden Werten
*/
@Test
@SuppressWarnings("ConstantConditions")
void testCreateTodo() {
final var newTodo = new Todo(null, "test-todo");
when(repo.save(any())).thenReturn(new TodoEntity(5L, "test-todo"));
// create
final var result = this.service.create(newTodo);
// find out id
assertThat(result).extracting(Todo::id).isEqualTo(5L);
verify(repo).save(refEq(new TodoEntity(null, "test-todo")));
}
/*
* Testfall:
* - Ändern eines bestehenden Todos
* - Aufruf der Repo-Methode und Rückgabewert prüfen
*/
@Test
@SuppressWarnings("ConstantConditions")
void testUpdateExisting() {
final var todo = new Todo(5L, "test-todo");
when(repo.existsById(todo.id())).thenReturn(true);
// Test
this.service.update(todo);
// Assert
verify(repo).save(refEq(new TodoEntity(5L, "test-todo")));
}
/*
* Testfall:
* - Ändern eines nicht existenten Todos
* - Aufruf der Repo-Methode und Rückgabewert prüfen
*/
@Test
void testUpdateNotExisting() {
final var todo = new Todo(5L, "test-todo");
when(repo.existsById(todo.id())).thenReturn(false);
// Test+Assert
assertThatThrownBy(() -> this.service.update(todo))
.isInstanceOf(NotFoundException.class);
verify(repo).existsById(todo.id());
verifyNoMoreInteractions(repo);
}
/*
* Testfall:
* - Löschen eines existenten Todos
* - Aufruf der Repo-Methode und Rückgabewert prüfen
*/
@Test
void testDeleteExisting() {
when(repo.existsById(5L)).thenReturn(true);
// Test
this.service.delete(5L);
// Assert
verify(repo).deleteById(5L);
}
/*
* Testfall:
* - Löschen eines nicht existenten Todos
* - Aufruf der Repo-Methode und Rückgabewert prüfen
*/
@Test
void testDeleteNotExisting() {
when(repo.existsById(5L)).thenReturn(false);
// Test+Assert
assertThatThrownBy(() -> this.service.delete(5L))
.isInstanceOf(NotFoundException.class);
verify(repo).existsById(5L);
verifyNoMoreInteractions(repo);
}
}
@@ -0,0 +1,15 @@
package com.baeldung.sample.shared;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@TestConfiguration
public class EnableBeanValidation {
@Bean
public MethodValidationPostProcessor validator() {
return new MethodValidationPostProcessor();
}
}