Java >> Java Tutorial >  >> Tag >> Spring

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 ein StatusResultMatchers zurück Objekt, das es uns erlaubt, Assertionen für den zurückgegebenen HTTP-Status zu schreiben.
  • Der content() -Methode gibt ein ContentResultMatchers zurück -Objekt, das es uns ermöglicht, Zusicherungen für den Inhalt der zurückgegebenen HTTP-Antwort zu schreiben.
  • Die jsonPath() Methode gibt einen JsonPathResultMatchers 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:

  1. 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 einen ResultActions zurückgibt Objekt.
  2. Sende einen GET Anfrage an den Pfad:'/todo-item/{id}' durch Aufrufen von perform() Methode des MockMvc Klasse. Denken Sie daran, ResultActions zurückzugeben Objekt, das von perform() 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:

  1. 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.
  2. Fügen Sie eine innere Klasse namens WhenRequestedTodoItemIsNotFound hinzu zum FindById 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.
  3. Fügen Sie eine innere Klasse namens WhenRequestedTodoItemIsFound hinzu zum FindById 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:

  1. Fügen Sie eine Konstante namens TODO_ITEM_ID hinzu zum FindById class . Diese Konstante gibt die ID des angeforderten Aufgabenelements an. Wir müssen diese Konstante zu FindById hinzufügen -Klasse, da ihr Wert von den Testmethoden verwendet wird, die in WhenRequestedTodoItemIsNotFound gefunden werden und WhenRequestedTodoItemIsFound Klassen.
  2. 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 der TodoItemCrudService Objekt wirft einen TodoItemNotFoundException wenn es findById() ist Methode wird mit dem Argument aufgerufen:1L .
  3. Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 404 zurückgibt.
  4. 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:

  1. Fügen Sie die erforderlichen Konstanten zum WhenRequestedTodoItemIsFound hinzu Klasse. Diese Konstanten geben die Eigenschaftswerte des gefundenen ToDo-Elements an.
  2. 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 der TodoItemCrudService Das Objekt gibt die Informationen des gefundenen Todo-Elements zurück, wenn es findById() ist Die Methode wird mit folgendem Argument aufgerufen:1L .
  3. Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 200 zurückgibt.
  4. Vergewissern Sie sich, dass das zu testende System die Informationen des gefundenen Todo-Elements als JSON zurückgibt.
  5. Stellen Sie sicher, dass das zu testende System die Informationen des gefundenen ToDo-Elements zurückgibt.
  6. Überprüfen Sie, ob das zu testende System die Informationen eines Todo-Elements mit einem Tag zurückgibt.
  7. 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 des MockMvcResultMatchers Klasse.
  • Wenn wir Zusicherungen für den Inhalt der zurückgegebenen HTTP-Antwort schreiben wollen, müssen wir den content() aufrufen Methode des MockMvcResultMatchers 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 des MockMvcResultMatchers 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 und hamcrest-library Abhängigkeiten werden aus dem Klassenpfad gefunden

Java-Tag