본문 바로가기
Spring Boot

SpringBoot를 활용한 Rest api 만들기(9) - Spring Starter Unit Test

by ZIAHO 2023. 3. 30.

이번 시간에는 지금까지 만든 api를 Unit Test 하여 검증해보는 시간을 가지겠습니다.

 

현재 개발한 api는 별다른 로직이 없dj Unit Test가 공수 대비 효용성이 없어보이지만, 서버 프로세스의 요건이 복잡해지고 소스의 양이 늘어날 수록 사람의 눈으로 찾아낼 수 있는 오류의 범위는 한정되어 있으므로 시간이 지날수록 코드의 안정성을 유지하기 힘들게 됩니다.

그래서 서비스의 규모가 작을때 부터 중요한 프로세스에 대해서는 자동화된 테스트 환경을 구축해 놓는것이 품질 유지에 매우 효과적입니다.


Spring에서는 자체적으로 유닛 테스트를 지원하는 라이브러리를 제공하고 있습니다.

spring-boot-starter-test에는 JUnit, Mockito, Hamcrest 등을 이미 포함하고 있으므로 따로 설정할 필요가 없습니다.

SpringSecurity는 별도로 test를 제공하므로 해당 라이브러리를 명시하여 다운로드 합니다.

// build.gradle
testImplementation("org.springframework.security:spring-security-test")

 

테스트 코드 작성은 src/test/java 아래에 작성합니다.

 

테스트 클래스의 생성은 테스트하고자 하는 클래스로 이동하여 소스의 Class명에 커서를 두고 Ctrl + Shift + T를 눌러 테스트 생성 메뉴를 호출하여 생성합니다.


JPA Test를 위한 @DataJpaTest

SpringBoot Test에서는 @DataJpaTest라는 어노테이션을 지원하는데, 해당 어노테이션은 JPA 테스트를 위한 손쉬운 환경을 만들어 줍니다.

해당 어노테이션은 다른 컴포넌트들은 로드하지 않고 @Entity를 읽어 Repository 내용을 테스트할 수 있는 환경을 만들어 줍니다. 또한 @Transactional을 포함하고 있어 테스트가 완료되면 따로 롤백을 할 필요가 없습니다.

아래 테스트에서는 회원을 생성하고 조회하여 둘 간의 데이터가 맞는지 비교하는 테스트입니다.

JPA Repo의 단위 테스트가 필요하다면 @DataJpaTest를 이용하는 것이 효과적입니다.

참고로 @AutoConfigureTestDatabase(replace = Replace.NONE) 라는 어노테이션의 속성을 추가하면 메모리 데이터베이스가 아닌 실 데이터베이스에서의 테스트도 가능합니다.

*Junit5에선 클래스명 앞에 public을 생략해도 됩니다.

package org.ziaho.ziahorestapi.repo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.ziaho.ziahorestapi.entity.User;

import java.util.Collections;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
class UserJpaRepoTest {

    @Autowired
    private UserJpaRepo userJpaRepo;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void whenFindByUid_thenReturnUser() {
        String uid = "test@test.com";
        String name = "test1";

        // given
        userJpaRepo.save(User.builder()
                .uid(uid)
                .password(passwordEncoder.encode("1234"))
                .name(name)
                .roles(Collections.singletonList("ROLE_USER"))
                .build());
        // when
        Optional<User> user = userJpaRepo.findByUid(uid);
        // then
        assertNotNull(user); // user객체가 null이 아닌지 체크
        assertTrue(user.isPresent()); // user객체의 존재여부 true/false 체크
        assertEquals(user.get().getName(), name); // user 객체의 name과 name변수 값이 같은지 체크
    }

}

 

SpringBoot Test

@SpringBootTest 어노테이션의 설정만으로 Boot의 Configuration들을 자동으로 설정할 수 있습니다.

classes 설정으로 일부만 로드할 수도 있습니다. 명시하지 않으면 Config에 명시된 모든 Bean이 로드됩니다.

 

@AutoConfigureMockMvc는 Controller 테스트시 MockMvc를 간편하게 사용할 수 있도록 해줍니다.

아래는 MockMvc를 사용하여 SignController의 로그인, 가입을 테스트 하는 예제입니다.

package org.ziaho.ziahorestapi.controller.v1;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.web.JsonPath;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.ziaho.ziahorestapi.entity.User;
import org.ziaho.ziahorestapi.repo.UserJpaRepo;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class SignControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserJpaRepo userJpaRepo;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @BeforeEach
    public void setUp() throws Exception {
        userJpaRepo.save(User.builder()
                .uid("test@test.com")
                .name("test1")
                .password(passwordEncoder.encode("1234"))
                .roles(Collections.singletonList("ROEL_USER"))
                .build());
    }

    @Test
    public void signin() throws Exception {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("id", "test@test.com");
        params.add("password", "1234");
        mockMvc.perform(post("/v1/signin").params(params))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.msg").exists())
                .andExpect(jsonPath("$.data").exists());
    }

    @Test
    public void signup() throws Exception {
        long epochTime = LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond();
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("id", "test_" + epochTime + "@test.com");
        params.add("password", "12345");
        params.add("name", "test_" + epochTime);
        mockMvc.perform(post("/v1/signup").params(params))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.msg").exists());
    }
}

 

SpringSecurity Test

SpringSecurity는 유저에게 리소스의 사용 권한이 있는지의 상태에 따른 테스트가 필요합니다.

@WithMockUser 어노테이션은 그 기능을 쉽게 처리할 수 있게 도와줍니다.

아래 예제는 ADMIN 권한을 가진 가상의 회원을 만들어 USER 권한으로만 접근할 수 있는 리소스를 요청했을 때 accessDenied가 발생하는 상황을 구현한 것입니다.

package org.ziaho.ziahorestapi.controller.v1;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    private String token;

    @Test
    @WithMockUser(username = "mockUser", roles = {"ADMIN"}) // 가상의 Mock 유저 대입
    public void accessdenied() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders
                        .get("/v1/users"))
                //.header("X-AUTH-TOKEN", token))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(forwardedUrl("/exception/accessdenied"));
    }

실행 해보기

해당 테스트 클래스 명 왼쪽의 초록 삼각형을 클릭하면 테스트 클래스를 실행시켜 볼 수 있습니다.

그러면 다음과 같이 실행이 된 것을 볼 수 있습니다.

 

signin() 메소드의 결과를 대표로 보고 포스팅을 마치도록 하겠습니다.

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /v1/signin
       Parameters = {id=[test@test.com], password=[1234]}
          Headers = []
             Body = null
    Session Attrs = {}
    
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = application/json
             Body = {"success":true,"code":0,"msg":"성공하였습니다.","data":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzMyIsInJvbGVzIjpbIlJPRUxfVVNFUiJdLCJpYXQiOjE2ODAxNzg5MDgsImV4cCI6MTY4MDE4MjUwOH0.kuDp5szfUOxaQl7al1owsWWrpz_PYm29HtSVVjxC5uI"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

해당 Request/Response 외에도 Handler 등 다른 요소들도 출력되지만 앞선 포스팅에서 중점적으로 다루어왔던 내용들만 작성하였습니다.

 

id와 password를 파라미터로 요청하였고, 성공하였다는 메세지와 함께 JWT 토큰값이 정상적으로 발급 된 것을 볼 수 있습니다.


단위 테스트와 관련된 해당 포스팅을 진행 하면서, 사실 단위 테스트에 대한 내용을 처음 접해보았기 때문에

작성을 하면서도 무슨말인지 잘 모르는 경우가 많았다.

그래서 Junit의 전체적인 개념과 해당 포스팅에서 사용한 assert메소드, MockMvc와 그의 메소드등은 따로 포스팅하여 학습하고 기록 할 예정이다.

단위유닛 테스트에 대한 추가적인 포스팅이 끝나고 어느정도 이해가 되고 나면, 오늘 포스팅에서 다룬 메소들 외에 나머지 메소드들의 테스트 소스를 수정해보려고 한다.


출처

SpringBoot2로 Rest api 만들기(9) – Spring Starter Unit Test SpringBoot (daddyprogrammer.org)

 

SpringBoot2로 Rest api 만들기(9) – Spring Starter Unit Test SpringBoot

이번 시간에는 지금까지 만든 api를 Unit Test 하여 검증해보는 시간을 갖겠습니다. 현재 개발한 api는 별다른 로직이 없어 Unit Test가 공수 대비 효용성이 없어 보입니다. 하지만 서버 프로세스의 요

www.daddyprogrammer.org

 

codej99/SpringRestApi at feature/junit-test (github.com)

 

GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

SpringBoot2, SpringSecurity, JWT, Stateless Restful API - GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

github.com