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

Unit Tests für eine Spring MVC REST API schreiben:Zurückgeben 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 als JSON zurückgeben. Dieser Blogbeitrag enthält weitere Informationen zum Schreiben von Komponententests für eine Spring MVC-REST-API. Genauer gesagt beschreibt dieser Blogbeitrag, wie wir Komponententests für einen Spring MVC-Controller schreiben können, der eine Liste 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-Anforderungen verarbeitet, die an den Pfad „/todo-item“ gesendet werden. Der Vertrag dieses API-Endpunkts wird im Folgenden beschrieben:

  • Das zu testende System gibt immer den HTTP-Statuscode 200 zurück.
  • Wenn ToDo-Elemente gefunden werden, erstellt das zu testende System ein JSON-Dokument, das eine Liste der gefundenen ToDo-Elemente enthält, und fügt dieses Dokument dem Hauptteil der zurückgegebenen HTTP-Antwort hinzu.
  • Wenn keine Aufgaben gefunden werden, erstellt das zu testende System ein JSON-Dokument, das eine leere Liste enthält, und fügt dieses Dokument dem Hauptteil der zurückgegebenen HTTP-Antwort hinzu.

Die getestete Controller-Methode heißt findAll() und es gibt einfach die Todo-Elemente zurück, die aus der Datenbank gefunden werden. 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/todo-item")
public class TodoItemCrudController {
    private final TodoItemCrudService service;

    @Autowired
    public TodoItemCrudController(TodoItemCrudService service) {
        this.service = service;
    }

    @GetMapping
    public List<TodoListItemDTO> findAll() {
        return service.findAll();
    }
}

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
}

Wenn beispielsweise zwei Todo-Elemente in der Datenbank gefunden werden, gibt das getestete System das folgende JSON-Dokument an den Client zurück:

[
	{
		"id":1,
		"title":"Write example application",
		"status":"DONE"
	},
	{
		"id":2,
		"title":"Write blog post",
		"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 eine Liste 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 der 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.
  • 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.
  • Der 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-Requests an das zu testende System sendet.

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. Stellen Sie sicher, dass diese Methode einen ResultActions zurückgibt Objekt.
  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 WhenNoTodoItemsAreFound 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 WhenTwoTodoItemsAreFound 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);

        TodoItemCrudController testedController = new TodoItemCrudController(service);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
                .setControllerAdvice(new TodoItemErrorHandler())
                .setMessageConverters(objectMapperHttpMessageConverter())
                .build();
        requestBuilder = new TodoItemRequestBuilder(mockMvc);
    }

    @Nested
    @DisplayName("Find all todo items")
    class FindAll {
        
        @Nested
        @DisplayName("When no todo items are found")
        class WhenNoTodoItemsAreFound {
            
        }

        @Nested
        @DisplayName("When two todo items are found")
        class WhenTwoTodoItemsAreFound {
            
        }
    }
}

Zweiter , 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 Informationen der gefundenen ToDo-Elemente als JSON zurückgibt.

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.http.MediaType;
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.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 all todo items")
    class FindAll {

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

        @Test
        @DisplayName("Should return the found todo items as JSON")
        void shouldReturnFoundTodoItemAsJSON() throws Exception {
            requestBuilder.findAll()
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
        }

        //The other 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 WhenNoTodoItemsAreFound 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 ein JSON-Dokument zurückgibt, das eine leere Liste enthält.

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.http.MediaType;
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.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 all todo items")
    class FindAll {

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

        @Test
        @DisplayName("Should return the found todo items as JSON")
        void shouldReturnFoundTodoItemAsJSON() throws Exception {
            requestBuilder.findAll()
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
        }

        @Nested
        @DisplayName("When no todo items are found")
        class WhenNoTodoItemsAreFound {

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

            @Test
            @DisplayName("Should return zero todo items")
            void shouldReturnZeroTodoItems() throws Exception {
                requestBuilder.findAll()
                        .andExpect(jsonPath("$", 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 WhenTwoTodoItemsAreFound hinzu Klasse. Diese Konstanten spezifizieren die Informationen der gefundenen Aufgaben.
  2. Fügen Sie dem WhenTwoTodoItemsAreFound 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 Das 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 ein JSON-Dokument zurückgibt, das zwei Aufgaben enthält.
  4. Stellen Sie sicher, dass das zu testende System die richtigen Informationen des ersten Todo-Elements zurückgibt.
  5. Stellen Sie sicher, dass das zu testende System die korrekten Informationen des zweiten Todo-Elements zurückgibt.

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.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 all todo items")
    class FindAll {

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

        @Test
        @DisplayName("Should return the found todo items as JSON")
        void shouldReturnFoundTodoItemAsJSON() throws Exception {
            requestBuilder.findAll()
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
        }

        //The other inner class is omitted

        @Nested
        @DisplayName("When two todo items are found")
        class WhenTwoTodoItemsAreFound {

            private static final Long FIRST_TODO_ITEM_ID = 1L;
            private static final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE;
            private static final String FIRST_TODO_ITEM_TITLE = "Write example application";

            private static final Long SECOND_TODO_ITEM_ID = 2L;
            private static final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS;
            private static final String SECOND_TODO_ITEM_TITLE = "Write blog post";

            @BeforeEach
            void returnTwoTodoItems() {
                TodoListItemDTO first = new TodoListItemDTO();
                first.setId(FIRST_TODO_ITEM_ID);
                first.setStatus(FIRST_TODO_ITEM_STATUS);
                first.setTitle(FIRST_TODO_ITEM_TITLE);

                TodoListItemDTO second = new TodoListItemDTO();
                second.setId(SECOND_TODO_ITEM_ID);
                second.setStatus(SECOND_TODO_ITEM_STATUS);
                second.setTitle(SECOND_TODO_ITEM_TITLE);

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

            @Test
            @DisplayName("Should return two todo items")
            void shouldReturnTwoTodoItems() throws Exception {
                requestBuilder.findAll()
                        .andExpect(jsonPath("$", hasSize(2)));
            }

            @Test
            @DisplayName("Should return the information of the first todo item")
            void shouldReturnInformationOfFirstTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(jsonPath("$[0].id",
                                equalTo(FIRST_TODO_ITEM_ID.intValue()))
                        )
                        .andExpect(jsonPath("$[0].status",
                                equalTo(FIRST_TODO_ITEM_STATUS.name()))
                        )
                        .andExpect(jsonPath("$[0].title",
                                equalTo(FIRST_TODO_ITEM_TITLE))
                        );
            }

            @Test
            @DisplayName("Should return the information of the second todo item")
            void shouldReturnInformationOfSecondTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(jsonPath("$[1].id",
                                equalTo(SECOND_TODO_ITEM_ID.intValue()))
                        )
                        .andExpect(jsonPath("$[1].status",
                                equalTo(SECOND_TODO_ITEM_STATUS.name()))
                        )
                        .andExpect(jsonPath("$[1].title",
                                equalTo(SECOND_TODO_ITEM_TITLE))
                        );
            }
        }
    }
}

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