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

Schreiben von Komponententests für Spring MVC-Controller:Rendern 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 rendert.

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 richtige Ansicht darstellt.
  • Verstehen Sie, wie wir sicherstellen können, dass unsere Modellattribute die richtigen Informationen enthalten.

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}'. Diese Methode gibt den HTTP-Statuscode 200 zurück und rendert die Informationen eines Todo-Elements, dessen ID als Wert von id angegeben ist Pfadvariable. Wenn das angeforderte Todo-Element nicht in der Datenbank gefunden wird, gibt diese Methode den HTTP-Statuscode 404 zurück und rendert die Nicht gefunden-Ansicht.

Die getestete Controller-Methode heißt findById() und es wird wie folgt implementiert:

  1. Finden Sie das Aufgabenelement aus der Datenbank, indem Sie findById() aufrufen Methode des TodoItemCrudService Klasse. Übergeben Sie die ID des ToDo-Elements als Argument an die aufgerufene Methode.
  2. Legen Sie das gefundene Aufgabenelement in ein Modellattribut namens todoItem .
  3. Gib den Namen der Ansicht ('todo-item/view') zurück, die die Informationen des gefundenen todo-Elements darstellt.

Der Quellcode der getesteten Controller-Methode sieht wie folgt aus:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/todo-item")
public class TodoItemCrudController {

    private final TodoItemCrudService service;

    @Autowired
    public TodoItemCrudController(TodoItemCrudService service) {
        this.service = service;
    }
    
    @GetMapping("{id}")
    public String findById(@PathVariable("id") Long id, Model model) {
        TodoItemDTO found = service.findById(id);
        model.addAttribute("todoItem", found);
        return "todo-item/view";
    }
}

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
}

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 Daten rendert, müssen wir lernen, wie wir Zusicherungen für die vom zu testenden System zurückgegebene Antwort schreiben können. Wenn wir Zusicherungen für die vom getesteten Spring MVC-Controller zurückgegebene Antwort schreiben möchten, müssen wir diese static verwenden Methoden des MockMvcResultMatchers Klasse:

  • Die status() Methode gibt einen StatusResultMatchers zurück Objekt, das es uns erlaubt, Assertionen für den zurückgegebenen HTTP-Status zu schreiben.
  • Die view() Methode gibt einen ViewResultMatchers zurück -Objekt, das es uns ermöglicht, Behauptungen für die gerenderte Ansicht zu schreiben.
  • Der model() Methode gibt einen ModelResultMatchers zurück -Objekt, mit dem wir Behauptungen für das Spring-MVC-Modell schreiben können.

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. Senden Sie einen GET Anfrage an den Pfad:'/todo-item/{id}' durch Aufrufen des 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);

        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(new TodoItemCrudController(service))
                .setHandlerExceptionResolvers(exceptionResolver())
                .setLocaleResolver(fixedLocaleResolver())
                .setViewResolvers(jspViewResolver())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }

    @Nested
    @DisplayName("Render the the information of the requested todo item")
    class FindById {

        @Nested
        @DisplayName("When the requested todo item isn't found from the database")
        class WhenRequestedTodoItemIsNotFound {

        }

        @Nested
        @DisplayName("When the requested todo item is found from the database")
        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 an FindById Klasse. Diese Konstante gibt die ID des angeforderten Aufgabenelements an. Wir müssen diese Konstante zu FindById hinzufügen Klasse, da ihr Wert von Testmethoden verwendet wird, die aus 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 die TodoItemCrudService Objekt wirft einen TodoItemNotFoundException wenn es findById() ist Die Methode wird mit folgendem Argument aufgerufen:99L .
  3. Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 404 zurückgibt.
  4. Stellen Sie sicher, dass das zu testende System die Ansicht „Nicht gefunden“ darstellt.

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.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

class TodoItemCrudControllerTest {

    private TodoItemRequestBuilder requestBuilder;
    private TodoItemCrudService service;

    @BeforeEach
    void configureSystemUnderTest() {
        service = mock(TodoItemCrudService.class);

        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(new TodoItemCrudController(service))
                .setHandlerExceptionResolvers(exceptionResolver())
                .setLocaleResolver(fixedLocaleResolver())
                .setViewResolvers(jspViewResolver())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }

    @Nested
    @DisplayName("Render the information of the requested todo item")
    class FindById {

        private final Long TODO_ITEM_ID = 99L;

        @Nested
        @DisplayName("When the requested todo item isn't found from the database")
        class WhenRequestedTodoItemIsNotFound {

            @BeforeEach
            void serviceThrowsNotFoundException() {
                given(service.findById(TODO_ITEM_ID))
                        .willThrow(new TodoItemNotFoundException(""));
            }

            @Test
            @DisplayName("Should return the HTTP status code 404")
            void shouldReturnHttpStatusCodeNotFound() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(status().isNotFound());
            }

            @Test
            @DisplayName("Should render the 404 view")
            void shouldRender404View() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(view().name("error/404"));
            }
        }

        //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 zu 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:99L .
  3. Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 200 zurückgibt.
  4. Überprüfen Sie, ob das zu testende System die Ansicht darstellt, die die Informationen des gefundenen ToDo-Elements anzeigt.
  5. Stellen Sie sicher, dass das zu testende System die Informationen des richtigen ToDo-Elements anzeigt.
  6. Vergewissern Sie sich, dass das zu testende System den richtigen Titel und die richtige Beschreibung anzeigt.
  7. Stellen Sie sicher, dass das zu testende System ein offenes Aufgabenelement anzeigt.
  8. Vergewissern Sie sich, dass das zu testende System ein Aufgabenelement mit einem Tag anzeigt.
  9. Stellen Sie sicher, dass das zu testende System die Informationen des gefundenen Tags anzeigt.

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 java.util.Collections;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasProperty;
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.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

class TodoItemCrudControllerTest {

    private TodoItemRequestBuilder requestBuilder;
    private TodoItemCrudService service;

    @BeforeEach
    void configureSystemUnderTest() {
        service = mock(TodoItemCrudService.class);

        MockMvc mockMvc = MockMvcBuilders
                .standaloneSetup(new TodoItemCrudController(service))
                .setHandlerExceptionResolvers(exceptionResolver())
                .setLocaleResolver(fixedLocaleResolver())
                .setViewResolvers(jspViewResolver())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }

    @Nested
    @DisplayName("Render the information of the requested todo item")
    class FindById {

        private final Long TODO_ITEM_ID = 99L;

        //The other inner class is omitted

        @Nested
        @DisplayName("When the requested todo item is found from the database")
        class WhenRequestedTodoItemIsFound {

            private final String TITLE = "Write example project";
            private final String DESCRIPTION = "Use JUnit 5";
            private final TodoItemStatus STATUS_OPEN = TodoItemStatus.OPEN;

            private final Long TAG_ID = 44L;
            private final String TAG_NAME = "tag";

            @BeforeEach
            void serviceReturnsOpenTodoItemWithOneTag() {
                TodoItemDTO found = new TodoItemDTO();
                found.setId(TODO_ITEM_ID);
                found.setTitle(TITLE);
                found.setDescription(DESCRIPTION);
                found.setStatus(STATUS_OPEN);

                TagDTO tag = new TagDTO();
                tag.setId(TAG_ID);
                tag.setName(TAG_NAME);
                found.setTags(Collections.singletonList(tag));

                given(service.findById(TODO_ITEM_ID)).willReturn(found);
            }

            @Test
            @DisplayName("Should return the HTTP status code 200")
            void shouldReturnHttpStatusCodeOk() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(status().isOk());
            }

            @Test
            @DisplayName("Should render the view todo item view")
            void shouldRenderViewTodoItemView() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(view().name("todo-item/view"));
            }

            @Test
            @DisplayName("Should display the information of the correct todo item")
            void shouldDisplayInformationOfCorrectTodoItem() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(model().attribute(
                                "todoItem",
                                hasProperty("id", equalTo(TODO_ITEM_ID))
                        ));
            }

            @Test
            @DisplayName("Should display the correct title and description")
            void shouldDisplayCorrectTitleAndDescription() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(model().attribute(
                                "todoItem",
                                allOf(
                                        hasProperty("title", equalTo(TITLE)),
                                        hasProperty("description",equalTo(DESCRIPTION))
                                )
                        ));
            }

            @Test
            @DisplayName("Should display an open todo item")
            void shouldDisplayOpenTodoItem() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(model().attribute(
                                "todoItem",
                                hasProperty("status", equalTo(STATUS_OPEN))
                        ));
            }

            @Test
            @DisplayName("Should display a todo item that has one tag")
            void shouldDisplayTodoItemThatHasOneTag() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(model().attribute(
                                "todoItem",
                                hasProperty("tags", hasSize(1))
                        ));
            }

            @Test
            @DisplayName("Should display the information of the found tag")
            void shouldDisplayInformationOfFoundTag() throws Exception {
                requestBuilder.findById(TODO_ITEM_ID)
                        .andExpect(model().attribute(
                                "todoItem",
                                hasProperty("tags", hasItem(
                                        allOf(
                                                hasProperty("id", equalTo(TAG_ID)),
                                                hasProperty("name", equalTo(TAG_NAME))
                                        )
                                ))
                        ));
            }
        }
    }
}

Wir können jetzt Unit-Tests für eine Controller-Methode schreiben, die die Informationen eines einzelnen Elements rendert. 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 die gerenderte Ansicht schreiben wollen, müssen wir den view() aufrufen Methode des MockMvcResultMatchers Klasse.
  • Wenn wir Zusicherungen für das Spring MVC-Modell schreiben wollen, müssen wir model() aufrufen Methode des MockMvcResultMatchers Klasse.
  • Wir können Hamcrest-Matcher verwenden, um Behauptungen für die Modellattribute zu schreiben, die aus dem Spring-MVC-Modell gefunden wurden.

Java-Tag