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

Schreiben von Komponententests für Spring MVC-Controller:Rendern einer Liste

Im vorherigen Teil meines Spring MVC Test-Tutorials wurde beschrieben, wie wir Komponententests für Spring MVC-Controller schreiben können, die die Informationen eines einzelnen Elements rendern. Dieser Blogbeitrag enthält weitere Informationen zum Schreiben von Komponententests für Spring MVC-Controller, die eine Ansicht rendern. Genauer gesagt beschreibt dieser Blogbeitrag, wie wir Komponententests für einen Spring MVC-Controller schreiben können, der eine Liste 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'. Diese Methode gibt den HTTP-Statuscode 200 zurück und rendert die Informationen aller Aufgaben, die in der Datenbank gefunden werden. Wenn keine Todo-Elemente in der Datenbank gefunden werden, gibt diese Controller-Methode den HTTP-Statuscode 200 zurück und rendert eine leere Liste.

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

  1. Suchen Sie die Todo-Einträge aus der Datenbank, indem Sie findAll() aufrufen Methode des TodoItemCrudService Klasse.
  2. Stellen Sie die gefundenen Todo-Elemente in ein Modellattribut namens todoItems .
  3. Gib den Namen der Ansicht ('todo-item/list') zurück, die die Informationen der gefundenen Todo-Elemente 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.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
    public String findAll(Model model) {
        List<TodoListItemDTO> todoItems = service.findAll();
        model.addAttribute("todoItems", todoItems);
        return "todo-item/list";
    }
}

Die TodoListItemDTO class ist ein DTO, das die Informationen eines einzelnen ToDo-Elements enthält. Sein Quellcode sieht wie folgt aus:

public class TodoListItemDTO {

    private Long id;
    private String title;
    private TodoItemStatus status;

    //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 der MockMvcResultMatchers Klasse:

  • Der 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.
  • Die 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 findAll() hinzu zu unserer Request-Builder-Klasse und stellen Sie sicher, dass diese Methode ein ResultActions-Objekt zurückgibt.
  2. Senden Sie einen GET Anfrage an den Pfad:'/todo-item' 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 findAll() throws Exception {
        return mockMvc.perform(get("/todo-item"));
    }
}

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 FindAll 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 WhenNoTodoItemsAreFoundFromDatabase hinzu zum FindAll Klasse. Diese innere Klasse enthält die Testmethoden, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn keine Todo-Elemente in der Datenbank gefunden werden.
  3. Fügen Sie eine innere Klasse namens WhenTwoTodoItemsAreFoundFromDatabase hinzu zum FindAll Klasse. Diese innere Klasse enthält die Testmethoden, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn zwei Todo-Elemente aus der Datenbank gefunden werden.

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 information of all todo items")
    class FindAll {
        
        @Nested
        @DisplayName("When no todo items are found from the database")
        class WhenNoTodoItemsAreFoundFromDatabase {
            
        }

        @Nested
        @DisplayName("When two todo items are found from the database")
        class WhenTwoTodoItemsAreFoundFromDatabase {
            
        }
    }
}

Zweite , da wir unserer Testklasse keinen doppelten Code hinzufügen möchten, werden wir dem FindAll einige Testmethoden hinzufügen Klasse. Diese Unit-Tests spezifizieren das Verhalten des zu testenden Systems in allen möglichen Szenarien. Wir können diese Komponententests schreiben, indem wir diesen Schritten folgen:

  1. Stellen Sie sicher, dass das zu testende System den HTTP-Statuscode 200 zurückgibt.
  2. Vergewissern Sie sich, dass das zu testende System die Listenansicht darstellt.

Nachdem wir diese Unit-Tests 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.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 all todo items")
    class FindAll {

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

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

        //The inner classes are omitted
    }
}

Dritter , müssen wir die Komponententests schreiben, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn keine Todo-Elemente in der Datenbank gefunden werden. Wir können die erforderlichen Testmethoden schreiben, indem wir diesen Schritten folgen:

  1. Fügen Sie dem WhenNoTodoItemsAreFoundFromDatabase eine neue Einrichtungsmethode hinzu -Klasse und stellen Sie sicher, dass sie ausgeführt wird, bevor eine Testmethode ausgeführt wird. Wenn wir diese Methode implementieren, müssen wir sicherstellen, dass der TodoItemCrudService Objekt gibt eine leere Liste zurück, wenn es findAll() ist Methode aufgerufen wird.
  2. Stellen Sie sicher, dass das zu testende System null Aufgaben anzeigt.

Nachdem wir die erforderlichen Unit-Tests 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.ArrayList;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
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 all todo items")
    class FindAll {

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

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

        @Nested
        @DisplayName("When no todo items are found from the database")
        class WhenNoTodoItemsAreFoundFromDatabase {

            @BeforeEach
            void serviceReturnsEmptyList() {
                given(service.findAll()).willReturn(new ArrayList<>());
            }

            @Test
            @DisplayName("Should display zero todo items")
            void shouldDisplayZeroTodoItems() throws Exception {
                requestBuilder.findAll().andExpect(model().attribute(
                        "todoItems", 
                        hasSize(0)
                ));
            }
        }

        //The other inner class is omitted
    }
}

Vierter , müssen wir die Komponententests schreiben, die sicherstellen, dass das zu testende System wie erwartet funktioniert, wenn zwei Todo-Elemente aus der Datenbank gefunden werden. Wir können die erforderlichen Testmethoden schreiben, indem wir diesen Schritten folgen:

  1. Fügen Sie die erforderlichen Konstanten zu WhenTwoTodoItemsAreFoundFromDatabase hinzu Klasse. Diese Konstanten spezifizieren die Informationen der gefundenen Aufgaben.
  2. Fügen Sie dem WhenTwoTodoItemsAreFoundFromDatabase eine neue Einrichtungsmethode hinzu -Klasse und stellen Sie sicher, dass sie ausgeführt wird, bevor eine Testmethode ausgeführt wird. Wenn wir diese Methode implementieren, müssen wir sicherstellen, dass der TodoItemCrudService -Objekt gibt eine Liste zurück, die zwei Aufgaben enthält, wenn es findAll() ist Methode aufgerufen wird.
  3. Stellen Sie sicher, dass das zu testende System zwei Aufgaben anzeigt.
  4. Vergewissern Sie sich, dass das zu testende System die korrekten Informationen des ersten ToDo-Elements anzeigt.
  5. Stellen Sie sicher, dass das zu testende System die korrekten Informationen des zweiten ToDo-Elements anzeigt.
  6. Vergewissern Sie sich, dass das zu testende System die Aufgaben in der richtigen Reihenfolge anzeigt.

Nachdem wir die erforderlichen Unit-Tests 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.Arrays;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
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 all todo items")
    class FindAll {

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

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

        //The other inner class is omitted

        @Nested
        @DisplayName("When two todo items are found from the database")
        class WhenTwoTodoItemsAreFoundFromDatabase {

            private final Long TODO_ITEM_ONE_ID = 1L;
            private final String TODO_ITEM_ONE_TITLE = "first todo item";
            private final Long TODO_ITEM_TWO_ID = 2L;
            private final String TODO_ITEM_TWO_TITLE = "second todo item";

            private final TodoItemStatus STATUS_OPEN = TodoItemStatus.OPEN;

            @BeforeEach
            void serviceReturnsTwoTodoItems() {
                TodoListItemDTO first = new TodoListItemDTO();
                first.setId(TODO_ITEM_ONE_ID);
                first.setTitle(TODO_ITEM_ONE_TITLE);
                first.setStatus(STATUS_OPEN);

                TodoListItemDTO second = new TodoListItemDTO();
                second.setId(TODO_ITEM_TWO_ID);
                second.setTitle(TODO_ITEM_TWO_TITLE);
                second.setStatus(STATUS_OPEN);

                given(service.findAll()).willReturn(Arrays.asList(first, second));
            }

            @Test
            @DisplayName("Should display two todo items")
            void shouldDisplayTwoTodoItems() throws Exception {
                requestBuilder.findAll().andExpect(model().attribute(
                        "todoItems",
                        hasSize(2)
                ));
            }

            @Test
            @DisplayName("Should display the information of the first todo item")
            void shouldDisplayInformationOfFirstTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems", 
                                        hasItem(allOf(
                                                hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
                                                hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
                                                hasProperty("status", equalTo(STATUS_OPEN))
                                        ))
                                )
                        );
            }

            @Test
            @DisplayName("Should display the information of the second todo item")
            void shouldDisplayInformationOfSecondTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems",
                                        hasItem(allOf(
                                                hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
                                                hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
                                                hasProperty("status", equalTo(STATUS_OPEN))
                                        ))
                                )
                        );
            }

            @Test
            @DisplayName("Should display the todo items in the correct order")
            void shouldDisplayFirstAndSecondTodoItemInCorrectOrder() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems",
                                        contains(
                                                allOf(
                                                        hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
                                                        hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
                                                        hasProperty("status", equalTo(STATUS_OPEN))
                                                ),
                                                allOf(
                                                        hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
                                                        hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
                                                        hasProperty("status", equalTo(STATUS_OPEN))
                                                )
                                        )
                                )
                        );
            }
        }
    }
}

Wir können jetzt Unit-Tests für eine Controller-Methode schreiben, die eine Liste 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