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:
+13
@@ -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 {
|
||||
}
|
||||
+218
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -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 {
|
||||
}
|
||||
+39
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+126
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user