Schreiben von Komponententests für eine Spring MVC REST API:Zurückgeben eines einzelnen Elements
Im vorherigen Teil meines Spring MVC Test-Tutorials wurde beschrieben, wie wir HTTP-Anforderungen an das zu testende System senden und Zusicherungen für die von der getesteten Controller-Methode zurückgegebene Antwort schreiben können. Dieser Blogbeitrag beschreibt, wie wir die im vorherigen Teil dieses Tutorials bereitgestellten Informationen verwenden können, wenn wir Komponententests für eine Controller-Methode schreiben, die die Informationen eines einzelnen Elements als JSON zurückgibt.
Nachdem wir diesen Blogbeitrag fertiggestellt haben, werden wir:
- Wissen, wie wir sicherstellen können, dass das zu testende System den korrekten HTTP-Statuscode zurückgibt.
- Kann überprüfen, ob das zu testende System die richtigen Informationen zurückgibt.
Fangen wir an.
Einführung in das zu testende System
Wir müssen Komponententests für eine Controller-Methode schreiben, die GET
verarbeitet Anfragen werden an den Pfad gesendet:'/todo-item/{id}'. Der Vertrag dieses API-Endpunkts wird im Folgenden beschrieben:
- Wenn das angeforderte Todo-Element gefunden wird, gibt das zu testende System den HTTP-Statuscode 200 zurück. Das zu testende System erstellt außerdem ein JSON-Dokument, das die Informationen des gefundenen Todo-Elements enthält, und fügt dieses Dokument dem Hauptteil des zurückgegebenen hinzu HTTP-Antwort.
- Wenn das angeforderte Aufgabenelement nicht gefunden wird, gibt das getestete System den HTTP-Statuscode 404 zurück. Da kein Aufgabenelement gefunden wird, ist der Text der zurückgegebenen HTTP-Antwort leer.
Die getestete Controller-Methode heißt findById()
und es gibt einfach die Informationen des Todo-Elements zurück, das aus der Datenbank gefunden wurde. Der Quellcode der getesteten Controller-Methode sieht wie folgt aus:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/todo-item") public class TodoItemCrudController { private final TodoItemCrudService service; @Autowired public TodoItemCrudController(TodoItemCrudService service) { this.service = service; } @GetMapping("{id}") public TodoItemDTO findById(@PathVariable("id") Long id) { return service.findById(id); } }
Die TodoItemDTO
class ist ein DTO, das die Informationen eines einzelnen ToDo-Elements enthält. Sein Quellcode sieht wie folgt aus:
public class TodoItemDTO { private Long id; private String description; private List<TagDTO> tags; private String title; private TodoItemStatus status; //Getters and setters are omitted }
Die TagDTO
Klasse ist ein DTO, das die Informationen eines einzelnen Tags enthält. Sein Quellcode sieht wie folgt aus:
public class TagDTO { private Long id; private String name; //Getters and setters are omitted }
Die TodoItemStatus
enum gibt die möglichen Status eines Aufgabeneintrags an. Sein Quellcode sieht wie folgt aus:
public enum TodoItemStatus { OPEN, IN_PROGRESS, DONE }
Wenn beispielsweise das gefundene Aufgabenelement in Bearbeitung ist und ein Tag hat, wird das folgende JSON-Dokument an den Client zurückgesendet:
{ "id":1, "description":"Remember to use JUnit 5", "tags":[ { "id":9, "name":"Code" } ], "title":"Write example application", "status":"IN_PROGRESS" }
Als Nächstes lernen wir, wie wir Assertionen für die vom zu testenden System zurückgegebene Antwort schreiben können.
Schreiben von Zusicherungen für die Antwort, die vom zu testenden System zurückgegeben wird
Bevor wir Komponententests für einen Spring MVC-Controller schreiben können, der die Informationen eines einzelnen Elements als JSON zurückgibt, müssen wir lernen, wie wir Zusicherungen für die vom zu testenden System zurückgegebene HTTP-Antwort schreiben können. Wenn wir Zusicherungen für die vom getesteten Spring MVC-Controller zurückgegebene HTTP-Antwort schreiben möchten, müssen wir diese static
verwenden Methoden des MockMvcResultMatchers
Klasse:
- Der
status()
Methode gibt einStatusResultMatchers
zurück Objekt, das es uns erlaubt, Assertionen für den zurückgegebenen HTTP-Status zu schreiben. - Der
content()
-Methode gibt einContentResultMatchers
zurück -Objekt, das es uns ermöglicht, Zusicherungen für den Inhalt der zurückgegebenen HTTP-Antwort zu schreiben. - Die
jsonPath()
Methode gibt einenJsonPathResultMatchers
zurück -Objekt, das es uns ermöglicht, Assertionen für den Text der zurückgegebenen HTTP-Antwort zu schreiben, indem wir JsonPath-Ausdrücke und Hamcrest-Matcher verwenden.
Da wir Behauptungen mithilfe von JsonPath-Ausdrücken und Hamcrest-Matchern schreiben, müssen wir sicherstellen, dass der json-path
und hamcrest-library
Abhängigkeiten werden aus dem Klassenpfad gefunden. Wenn wir die Maven- und Spring Boot-Abhängigkeitsverwaltung verwenden, können wir diese Abhängigkeiten deklarieren, indem wir das folgende XML-Snippet zu dependencies
hinzufügen Abschnitt unserer POM-Datei:
<dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <scope>test</scope> </dependency>
Lassen Sie uns weitermachen und herausfinden, wie wir eine Request-Builder-Methode schreiben können, die GET
sendet Anfragen an das zu testende System.
Schreiben einer neuen Request Builder-Methode
Da wir doppelten Code aus unserer Testklasse entfernen möchten, müssen wir HTTP-Anforderungen erstellen und an das zu testende System senden, indem wir eine sogenannte Request-Builder-Klasse verwenden. Mit anderen Worten, bevor wir Komponententests für das zu testende System schreiben können, müssen wir in eine Request-Builder-Methode schreiben, die HTTP-Anforderungen erstellt und an das zu testende System sendet. Wir können diese Request-Builder-Methode schreiben, indem wir diesen Schritten folgen:
- Fügen Sie eine neue Methode namens
findById()
hinzu zu unserer Request-Builder-Klasse. Stellen Sie sicher, dass diese Methode die ID des Todo-Elements als Methodenparameter verwendet und einenResultActions
zurückgibt Objekt. - Sende einen
GET
Anfrage an den Pfad:'/todo-item/{id}' durch Aufrufen vonperform()
Methode desMockMvc
Klasse. Denken Sie daran,ResultActions
zurückzugeben Objekt, das vonperform()
zurückgegeben wird Methode.
Nachdem wir unsere Request-Builder-Methode geschrieben haben, sieht der Quellcode unserer Request-Builder-Klasse wie folgt aus:
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; class TodoItemRequestBuilder { private final MockMvc mockMvc; TodoItemRequestBuilder(MockMvc mockMvc) { this.mockMvc = mockMvc; } ResultActions findById(Long id) throws Exception { return mockMvc.perform(get("/todo-item/{id}", id)); } }
Als Nächstes lernen wir, Unit-Tests für das zu testende System zu schreiben.
Einheitentests für das zu testende System schreiben
Wenn wir Unit-Tests für das zu testende System schreiben wollen, müssen wir diese Schritte befolgen:
Zuerst , müssen wir unserer Testklasse die erforderliche Klassenhierarchie hinzufügen. Da wir Komponententests schreiben, können wir diese Klassenhierarchie folgendermaßen erstellen:
- Fügen Sie eine innere Klasse namens
FindById
hinzu zu unserer Testklasse. Diese innere Klasse enthält die Testmethoden, die sicherstellen, dass das zu testende System wie erwartet funktioniert. - Fügen Sie eine innere Klasse namens
WhenRequestedTodoItemIsNotFound
hinzu zumFindById
Klasse. Diese innere Klasse enthält die Testmethoden, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn das angeforderte Aufgabenelement nicht in der Datenbank gefunden wird. - Fügen Sie eine innere Klasse namens
WhenRequestedTodoItemIsFound
hinzu zumFindById
Klasse. Diese innere Klasse enthält die Testmethoden, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn das angeforderte Aufgabenelement in der Datenbank gefunden wird.
Nachdem wir die erforderliche Klassenhierarchie erstellt haben, sieht der Quellcode unserer Testklasse wie folgt aus:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.mockito.Mockito.mock; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); TodoItemCrudController testedController = new TodoItemCrudController(service); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController) .setControllerAdvice(new TodoItemErrorHandler()) .setMessageConverters(objectMapperHttpMessageConverter()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Find todo item by using its id as search criteria") class FindById { @Nested @DisplayName("When the requested todo item isn't found") class WhenRequestedTodoItemIsNotFound { } @Nested @DisplayName("When the requested todo item is found") class WhenRequestedTodoItemIsFound { } } }
Zweite , müssen wir sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn das angeforderte Aufgabenelement nicht in der Datenbank gefunden wird. Wir können die erforderlichen Testmethoden schreiben, indem wir diesen Schritten folgen:
- Fügen Sie eine Konstante namens
TODO_ITEM_ID
hinzu zumFindById class
. Diese Konstante gibt die ID des angeforderten Aufgabenelements an. Wir müssen diese Konstante zuFindById
hinzufügen -Klasse, da ihr Wert von den Testmethoden verwendet wird, die inWhenRequestedTodoItemIsNotFound
gefunden werden undWhenRequestedTodoItemIsFound
Klassen. - Fügen Sie dem
WhenRequestedTodoItemIsNotFound
eine neue Einrichtungsmethode hinzu -Klasse und stellen Sie sicher, dass sie ausgeführt wird, bevor eine Testmethode ausgeführt wird. Wenn wir diese Einrichtungsmethode implementieren, müssen wir sicherstellen, dass derTodoItemCrudService
Objekt wirft einenTodoItemNotFoundException
wenn esfindById()
ist Methode wird mit dem Argument aufgerufen:1L
. - Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 404 zurückgibt.
- Vergewissern Sie sich, dass das zu testende System eine HTTP-Antwort mit leerem Antworttext zurückgibt.
Nachdem wir die erforderlichen Testmethoden geschrieben haben, sieht der Quellcode unserer Testklasse wie folgt aus:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); TodoItemCrudController testedController = new TodoItemCrudController(service); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController) .setControllerAdvice(new TodoItemErrorHandler()) .setMessageConverters(objectMapperHttpMessageConverter()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Find todo item by using its id as search criteria") class FindById { private static final Long TODO_ITEM_ID = 1L; @Nested @DisplayName("When the requested todo item isn't found") class WhenRequestedTodoItemIsNotFound { @BeforeEach void throwException() { given(service.findById(TODO_ITEM_ID)) .willThrow(new TodoItemNotFoundException("")); } @Test @DisplayName("Should return the HTTP status code not found (404)") void shouldReturnHttpStatusCodeNotFound() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(status().isNotFound()); } @Test @DisplayName("Should return HTTP response which has an empty response body") void shouldReturnHttpResponseWhichHasEmptyResponseBody() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(content().string("")); } } //The other inner class is omitted } }
Dritter , müssen wir sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn das angeforderte Aufgabenelement in der Datenbank gefunden wird. Wir können die erforderlichen Testmethoden schreiben, indem wir diesen Schritten folgen:
- Fügen Sie die erforderlichen Konstanten zum
WhenRequestedTodoItemIsFound
hinzu Klasse. Diese Konstanten geben die Eigenschaftswerte des gefundenen ToDo-Elements an. - Fügen Sie dem
WhenRequestedTodoItemIsFound
eine neue Einrichtungsmethode hinzu -Klasse und stellen Sie sicher, dass sie ausgeführt wird, bevor eine Testmethode ausgeführt wird. Wenn wir diese Einrichtungsmethode implementieren, müssen wir sicherstellen, dass derTodoItemCrudService
Das Objekt gibt die Informationen des gefundenen Todo-Elements zurück, wenn esfindById()
ist Die Methode wird mit folgendem Argument aufgerufen:1L
. - Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 200 zurückgibt.
- Vergewissern Sie sich, dass das zu testende System die Informationen des gefundenen Todo-Elements als JSON zurückgibt.
- Stellen Sie sicher, dass das zu testende System die Informationen des gefundenen ToDo-Elements zurückgibt.
- Überprüfen Sie, ob das zu testende System die Informationen eines Todo-Elements mit einem Tag zurückgibt.
- Stellen Sie sicher, dass das zu testende System die Informationen des gefundenen Tags zurückgibt.
Nachdem wir die erforderlichen Testmethoden geschrieben haben, sieht der Quellcode unserer Testklasse wie folgt aus:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.Arrays; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); TodoItemCrudController testedController = new TodoItemCrudController(service); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController) .setControllerAdvice(new TodoItemErrorHandler()) .setMessageConverters(objectMapperHttpMessageConverter()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Find todo item by using its id as search criteria") class FindById { private static final Long TODO_ITEM_ID = 1L; //The other inner class is omitted @Nested @DisplayName("When the requested todo item is found") class WhenRequestedTodoItemIsFound { private static final String DESCRIPTION = "Remember to use JUnit 5"; private static final Long TAG_ID = 9L; private static final String TAG_NAME = "Code"; private static final String TITLE = "Write example application"; private static final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS; @BeforeEach void returnFoundTodoItem() { TodoItemDTO found = new TodoItemDTO(); found.setId(TODO_ITEM_ID); found.setDescription(DESCRIPTION); found.setStatus(STATUS); found.setTitle(TITLE); TagDTO tag = new TagDTO(); tag.setId(TAG_ID); tag.setName(TAG_NAME); found.setTags(Arrays.asList(tag)); given(service.findById(TODO_ITEM_ID)).willReturn(found); } @Test @DisplayName("Should return the HTTP status code ok (200)") void shouldReturnHttpStatusCodeOk() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(status().isOk()); } @Test @DisplayName("Should return the information of the found todo item as JSON") void shouldReturnInformationOfFoundTodoItemAsJSON() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @Test @DisplayName("Should return the information of the found todo item") void shouldReturnInformationOfFoundTodoItem() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(jsonPath("$.id", equalTo(TODO_ITEM_ID.intValue())) ) .andExpect(jsonPath("$.description", equalTo(DESCRIPTION)) ) .andExpect(jsonPath("$.status", equalTo(STATUS.name())) ) .andExpect(jsonPath("$.title", equalTo(TITLE)) ); } @Test @DisplayName("Should return a todo item that has one tag") void shouldReturnTodoItemThatHasOneTag() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(jsonPath("$.tags", hasSize(1))); } @Test @DisplayName("Should return the information of the found tag") void shouldReturnInformationOfFoundTag() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(jsonPath("$.tags[0].id", equalTo(TAG_ID.intValue())) ) .andExpect(jsonPath("$.tags[0].name", equalTo(TAG_NAME)) ); } } } }
Wir können jetzt Unit-Tests für eine Controller-Methode schreiben, die die Informationen eines einzelnen Elements als JSON zurückgibt. Fassen wir zusammen, was wir aus diesem Blogbeitrag gelernt haben.
Zusammenfassung
Dieser Blogbeitrag hat uns vier Dinge gelehrt:
- Wenn wir Zusicherungen für den zurückgegebenen HTTP-Status schreiben wollen, müssen wir den
status()
aufrufen Methode desMockMvcResultMatchers
Klasse. - Wenn wir Zusicherungen für den Inhalt der zurückgegebenen HTTP-Antwort schreiben wollen, müssen wir den
content()
aufrufen Methode desMockMvcResultMatchers
Klasse. - Wenn wir Zusicherungen für den Hauptteil der zurückgegebenen HTTP-Antwort schreiben möchten, indem wir JsonPath-Ausdrücke und Hamcrest-Matcher verwenden, müssen wir den
jsonPath()
aufrufen Methode desMockMvcResultMatchers
Klasse. - Wenn wir Zusicherungen für den Hauptteil der zurückgegebenen HTTP-Antwort schreiben möchten, indem wir JsonPath-Ausdrücke und Hamcrest-Matcher verwenden, müssen wir sicherstellen, dass der
json-path
undhamcrest-library
Abhängigkeiten werden aus dem Klassenpfad gefunden