Spring MVC REST API için Birim Testleri Yazma: Liste Döndürme

In: Genel


Spring MVC Test öğreticimin önceki bölümü tek bir öğenin bilgilerini JSON olarak döndüren Spring MVC denetleyicileri için nasıl birim testleri yazabileceğimizi anlattı. Bu blog gönderisi, Spring MVC REST API için birim testleri yazma hakkında daha fazla bilgi sağlar. Daha spesifik olmak gerekirse, bu blog gönderisi, JSON olarak bir liste döndüren bir Spring MVC denetleyicisi için nasıl birim testleri yazabileceğimizi açıklar.

Bu blog gönderisini bitirdikten sonra:

  • Test edilen sistemin doğru HTTP durum kodunu döndürmesini nasıl sağlayabileceğimizi bilin.
  • Test edilen sistemin doğru bilgileri verdiğini doğrulayabilir.

Hadi başlayalım.

Bu blog yazısı şunları varsayar:

Test Edilen Sisteme Giriş

‘/todo-item’ yoluna gönderilen GET isteklerini işleyen bir denetleyici yöntemi için birim testleri yazmamız gerekiyor. Bu API uç noktasının sözleşmesi aşağıda açıklanmıştır:

  • Test edilen sistem her zaman HTTP durum kodu 200’ü döndürür.
  • Yapılacaklar öğeleri bulunursa, test edilen sistem, bulunan yapılacaklar listesini içeren bir JSON belgesi oluşturur ve bu belgeyi döndürülen HTTP yanıtının gövdesine ekler.
  • Yapılacaklar öğesi bulunamazsa, test edilen sistem boş bir liste içeren bir JSON belgesi oluşturur ve bu belgeyi döndürülen HTTP yanıtının gövdesine ekler.

Test edilen denetleyici yöntemi denir findAll() ve sadece veritabanından bulunan yapılacaklar öğelerini döndürür. Test edilen kontrolör yönteminin kaynak kodu aşağıdaki gibidir:


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();
    }
}

bu TodoListItemDTO class, tek bir yapılacaklar öğesinin bilgilerini içeren bir DTO’dur. Kaynak kodu şöyle görünür:


public class TodoListItemDTO {

    private Long id;
    private String title;
    private TodoItemStatus status;
    
    //Getters and setters are omitted
}

bu TodoItemStatus enum, yapılacaklar öğesinin olası durumlarını belirtir. Kaynak kodu şöyle görünür:


public enum TodoItemStatus {
    OPEN,
    IN_PROGRESS,
    DONE
}

Örneğin, veritabanından iki yapılacaklar öğesi bulunursa, test edilen sistem aşağıdaki JSON belgesini istemciye geri döndürür:


[
	{
		"id":1,
		"title":"Write example application",
		"status":"DONE"
	},
	{
		"id":2,
		"title":"Write blog post",
		"status":"IN_PROGRESS"
	}
]

Ardından, test edilen sistem tarafından döndürülen yanıt için nasıl iddia yazabileceğimizi öğreneceğiz.

Test Edilen Sistem Tarafından Döndürülen Yanıt için Beyan Yazma

JSON olarak bir liste döndüren bir Spring MVC denetleyicisi için birim testleri yazmadan önce, test edilen sistem tarafından döndürülen HTTP yanıtı için iddiaları nasıl yazabileceğimizi öğrenmeliyiz. Test edilen Spring MVC denetleyicisi tarafından döndürülen HTTP yanıtı için iddialar yazmak istediğimizde, bunları kullanmalıyız. static yöntemleri MockMvcResultMatchers sınıf:

  • bu status() yöntem bir döndürür StatusResultMatchers döndürülen HTTP durumu için iddialar yazmamıza izin veren nesne.
  • bu content() yöntem bir döndürür ContentResultMatchers döndürülen HTTP yanıtının içeriği için iddialar yazmamıza izin veren nesne.
  • bu jsonPath() yöntem bir döndürür JsonPathResultMatchers JsonPath ifadelerini ve Hamcrest eşleştiricilerini kullanarak döndürülen HTTP yanıtının gövdesi için iddialar yazmamıza izin veren nesne.

JsonPath ifadelerini ve Hamcrest eşleştiricilerini kullanarak iddialar yazdığımız için, json-path ve hamcrest-library bağımlılıklar sınıf yolundan bulunur. Maven ve Spring Boot bağımlılık yönetimini kullanıyorsak, aşağıdaki XML parçacığını ekleyerek bu bağımlılıkları bildirebiliriz. dependencies POM dosyamızın bölümü:


<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>

Devam edelim ve test edilen sisteme GET istekleri gönderen bir istek oluşturucu yöntemini nasıl yazabileceğimizi öğrenelim.

Yeni İstek Oluşturucu Yöntemi Yazma

Test sınıfımızdan yinelenen kodu kaldırmak istediğimiz için, istek oluşturucu sınıfı olarak adlandırılan bir sınıf kullanarak test edilen sisteme HTTP istekleri oluşturmalı ve göndermeliyiz. Başka bir deyişle, test edilen sistem için birim testleri yazmadan önce, test edilen sisteme HTTP istekleri oluşturan ve gönderen bir istek oluşturucu yöntemine yazmamız gerekir. Bu istek oluşturucu yöntemini aşağıdaki adımları izleyerek yazabiliriz:

  1. adlı yeni bir yöntem ekleyin findAll() istek oluşturucu sınıfımıza. Bu yöntemin bir değer döndürdüğünden emin olun. ResultActions nesne.
  2. Gönder GET yola istek: ‘/todo-item’ perform() yöntemi MockMvc sınıf. iade etmeyi unutmayın ResultActions tarafından döndürülen nesne perform() yöntem.

İstek oluşturucu yöntemimizi yazdıktan sonra istek oluşturucu sınıfımızın kaynak kodu aşağıdaki gibi görünür:


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"));
    }
}

Ardından, test edilen sistem için birim testleri yazmayı öğreneceğiz.

Test Edilen Sistem İçin Birim Testleri Yazma

Test edilen sistem için birim testleri yazmak istediğimizde şu adımları izlemeliyiz:

Öncelikle, gerekli sınıf hiyerarşisini test sınıfımıza eklemeliyiz. Birim testleri yazdığımız için aşağıdaki adımları izleyerek bu sınıf hiyerarşisini oluşturabiliriz:

  1. adlı bir iç sınıf ekleyin FindAll test sınıfımıza. Bu iç sınıf, test edilen sistemin beklendiği gibi çalışmasını sağlayan test yöntemlerini içerir.
  2. adlı bir iç sınıf ekleyin WhenNoTodoItemsAreFound için FindAll sınıf. Bu iç sınıf, veritabanında yapılacak hiçbir öğe bulunmadığında test edilen sistemin beklendiği gibi çalışmasını sağlayan test yöntemlerini içerir.
  3. adlı bir iç sınıf ekleyin WhenTwoTodoItemsAreFound için FindAll sınıf. Bu iç sınıf, veritabanından iki yapılacak iş öğesi bulunduğunda test edilen sistemin beklendiği gibi çalışmasını sağlayan test yöntemlerini içerir.

Gerekli sınıf hiyerarşisini oluşturduktan sonra test sınıfımızın kaynak kodu şu şekildedir:


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 {
            
        }
    }
}

İkincitest sınıfımıza mükerrer kod eklemek istemediğimiz için, test sınıfına bazı test yöntemleri ekleyeceğiz. FindAll sınıf. Bu birim testleri, olası tüm senaryolarda test edilen sistemin davranışını belirtir. Bu birim testlerini aşağıdaki adımları izleyerek yazabiliriz:

  1. Test edilen sistemin 200 HTTP durum kodunu döndürdüğünden emin olun.
  2. Test edilen sistemin, bulunan yapılacaklar öğelerinin bilgilerini JSON olarak döndürdüğünü doğrulayın.

Bu birim testleri yazdıktan sonra test sınıfımızın kaynak kodu şu şekilde görünür:


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
    }
}

Üçüncü, veritabanından yapılacak hiçbir öğe bulunmadığında test edilen sistemin beklendiği gibi çalışmasını sağlayan birim testleri yazmamız gerekiyor. Aşağıdaki adımları takip ederek gerekli test yöntemlerini yazabiliriz:

  1. için yeni bir kurulum yöntemi ekleyin. WhenNoTodoItemsAreFound sınıf ve bir test yöntemi çalıştırılmadan önce çalıştırıldığından emin olun. Bu yöntemi uyguladığımızda, TodoItemCrudService nesne, boş bir liste döndürür. findAll() yöntem çağrılır.
  2. Test edilen sistemin boş bir liste içeren bir JSON belgesi döndürdüğünden emin olun.

Gerekli birim testleri yazdıktan sonra test sınıfımızın kaynak kodu şu şekildedir:


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
    }
}

Dördüncü, veritabanından iki yapılacaklar öğesi bulunduğunda test edilen sistemin beklendiği gibi çalışmasını sağlayan birim testleri yazmamız gerekiyor. Aşağıdaki adımları takip ederek gerekli test yöntemlerini yazabiliriz:

  1. Gerekli sabitleri ekleyin WhenTwoTodoItemsAreFound sınıf. Bu sabitler, bulunan yapılacaklar öğelerinin bilgilerini belirtir.
  2. için yeni bir kurulum yöntemi ekleyin. WhenTwoTodoItemsAreFound sınıf ve bir test yöntemi çalıştırılmadan önce çalıştırıldığından emin olun. Bu yöntemi uyguladığımızda, TodoItemCrudService nesne, iki yapılacaklar öğesi içeren bir liste döndürür. findAll() yöntem çağrılır.
  3. Test edilen sistemin iki yapılacaklar öğesi içeren bir JSON belgesi döndürdüğünden emin olun.
  4. Test edilen sistemin, ilk yapılacaklar öğesinin doğru bilgilerini döndürdüğünü doğrulayın.
  5. Test edilen sistemin ikinci yapılacaklar öğesinin doğru bilgilerini döndürdüğünden emin olun.

Gerekli birim testleri yazdıktan sonra test sınıfımızın kaynak kodu şu şekildedir:


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))
                        );
            }
        }
    }
}

Artık bir listeyi JSON olarak döndüren bir denetleyici yöntemi için birim testleri yazabiliriz. Bu blog gönderisinden öğrendiklerimizi özetleyelim.

Özet

Bu blog yazısı bize dört şey öğretti:

  • Döndürülen HTTP durumu için iddialar yazmak istediğimizde, şunu çağırmamız gerekir: status() yöntemi MockMvcResultMatchers sınıf.
  • Döndürülen HTTP yanıtının içeriği için iddialar yazmak istediğimizde, şunu çağırmamız gerekir: content() yöntemi MockMvcResultMatchers sınıf.
  • JsonPath ifadelerini ve Hamcrest eşleştiricilerini kullanarak döndürülen HTTP yanıtının gövdesi için iddialar yazmak istediğimizde, şunu çağırmamız gerekir: jsonPath() yöntemi MockMvcResultMatchers sınıf.
  • JsonPath ifadelerini ve Hamcrest eşleştiricilerini kullanarak döndürülen HTTP yanıtının gövdesi için iddialar yazmak istiyorsak, json-path ve hamcrest-library bağımlılıklar sınıf yolundan bulunur

Not: Yapabilirsiniz bu blog gönderisinin örnek uygulamasını Github’dan alın.

Bir cevap yazın

Ready to Grow Your Business?

We Serve our Clients’ Best Interests with the Best Marketing Solutions. Find out More

How Can We Help You?

Need to bounce off ideas for an upcoming project or digital campaign? Looking to transform your business with the implementation of full potential digital marketing?

For any career inquiries, please visit our careers page here.
[contact-form-7 404 "Bulunamadı"]