이 연재글은 SpringBoot2로 Rest api 만들기의 9번째 글입니다.

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

현재 개발한 api는 별다른 로직이 없어 Unit Test가 공수 대비 효용성이 없어 보입니다. 하지만 서버 프로세스의 요건이 복잡해지고 소스의 양이 늘어날수록 사람의 눈으로 찾아낼 수 있는 오류의 범위는 한정되어 있으므로 시간이 지날수록 코드의 안정성을 유지하기 힘들게 됩니다. 그래서 서비스가 작을 때부터 중요한 프로세스에 대하여 자동화된 테스트 환경을 구축해 놓는 것이 코드 품질 유지에 매우 효과적입니다.

Spring에서는 자체적으로 유닛 테스트를 지원하는 라이브러리를 제공하고 있습니다. spring-boot-starter-test에는 JUnit, Mockito, Hamcrest 등을 이미 포함하고 있으므로 따로 설정할 필요가 없습니다. SpringSecurity는 별도로 test를 제공하므로 해당 라이브러리를 명시하여 다운로드합니다.

#build.gradle
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

테스트 코드 작성은 src/test/java 아래에 작성합니다. 테스트 클래스의 생성은 테스트하고자 하는 클래스로 이동하여 메뉴의 Navigate – Test를 선택하면 자동으로 생성할 수 있습니다. 또는 소스의 Class명에 커서를 두고 Ctrl+Shit+T를 눌러서 동일하게 테스트 생성 메뉴를 호출할 수 있습니다.

JPA Test를 위한 @DataJpaTest

SpringBoot Test에서는 @DataJpaTest라는 어노테이션을 지원하는데 해당 어노테이션은 JPA 테스트를 위한 손쉬운 환경을 만들어 줍니다. 해당 어노테이션은 다른 컴포넌트들은 로드하지 않고 @Entity를 읽어 Repository 내용을 테스트할 수 있는 환경을 만들어 줍니다. 또한 @Transactional을 포함하고 있어 테스트가 완료되면 따로 롤백을 할 필요가 없습니다. 아래 테스트에서는 회원을 생성하고 조회하여 둘 간의 데이터가 맞는지 비교하는 테스트입니다. JPA Repo의 단위 테스트가 필요하다면 @DataJpaTest를 이용하는 것이 효과적입니다. 참고로 @AutoConfigureTestDatabase(replace = Replace.NONE) 라는 어노테이션의 속성을 추가하면 메모리 데이터 베이스가 아닌 실 데이터베이스에 테스트도 가능합니다.

package com.rest.api.repo;

import com.rest.api.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
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.springframework.test.context.junit4.SpringRunner;

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

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserJpaRepoTest {

    @Autowired
    private UserJpaRepo userJpaRepo;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void whenFindByUid_thenReturnUser() {
        String uid = "angrydaddy@gmail.com";
        String name = "angrydaddy";
        // 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변수 값이 같은지 체크
        assertThat(user.get().getName(), is(name)); // user객체의 name과 name변수 값이 같은지 체크
    }
}

SpringBoot Test

@SpringBootTest 어노테이션의 설정만으로 Boot의 Configuration들을 자동으로 설정할 수 있습니다. 다음과 같이 classes 설정으로 일부만 로드할 수도 있습니다. 명시하지 않으면 Config에 명시된 모든 빈이 로드됩니다.
@SpringBootTest(classes = {CustomUserDetailService.class, SecurityConfiguration.class})
@AutoConfigureMockMvc은 Controller 테스트 시 MockMvc를 간편하게 사용할 수 있도록 해줍니다. 아래는 MockMvc를 사용하여 SignController의 로그인, 가입을 테스트하는 예제입니다.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class SignControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        userJpaRepo.save(User.builder().uid("happydaddy@naver.com").name("happydaddy").password(passwordEncoder.encode("1234")).roles(Collections.singletonList("ROLE_USER")).build());
    }

    @Test
    public void signin() throws Exception {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("id", "happydaddy@naver.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", "happydaddy_" + epochTime + "@naver.com");
        params.add("password", "12345");
        params.add("name", "happydaddy_" + 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가 발생하는 상황을 구현한 것입니다.

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

Junit assert 메서드

assertNotNull(obj), assertNotNull(obj)
객체(obj)의 Null여부를 테스트
assertTrue(condition), assertFalse(condition)
조건(condition)의 참/거짓 테스트
assertEquals(obj1, obj2), assertNotEquals(obj1, obj2)
obj1와 obj2의 값이 같은지 테스트
assertSame(obj1, obj2)
obj1과 obj2의 객체가 같은지 테스트
assertArrayEquals(arr1,arr2)
배열 arr1과 arr2가 같은지 테스트
assertThat(T actual, Matcher matcher)
첫 번째 인자에 비교대상 값을 넣고, 두 번째 로직에는 비교로직을 넣어 조건 테스트
ex) assertThat(a, is(100)) : a의 값이 100인가?
ex) assertThat(obj, is(nullValue())); 객체가 null인가?

MockMvc 메서드

perform

  • 주어진 url을 수행할 수 있는 환경을 구성해 줍니다.
  • GET, POST, PUT, DELETE 등 다양한 method 처리가 가능합니다.
  • header에 값을 세팅하거나 AcceptType 설정을 지원합니다.
  • mockMvc.perform(post(“/v1/signin”).params(params)

andDo

  • perform 요청을 처리합니다. andDo(print())를 하면 처리 결과를 console 화면에서 볼 수 있습니다.

andExpect

  • 검증 내용을 체크합니다.
  • 결과가 200 OK 인지 체크 – andExpect(status().isOK())
  • 결과 데이터가 Json인 경우 다음과 같이 체크 가능합니다.
    .andExpect(jsonPath(“$.success”).value(true))

andReturn

  • 테스트 완료 후 결과 객체를 받을 수 있습니다. 후속 작업이 필요할 때 용이합니다.
  • MvcResult result = mockMvc.perform(post(“/v1/signin”).params(params)).andDo(print());

여기까지 Spring의 단위 테스트를 위한 가장 기본적인 방법에 대해 살펴보았습니다. 적절한 어노테이션의 사용은 테스팅 환경을 쉽게 구축하는데 매우 효과적입니다. 매번 서버를 올렸다 내렸다 하면서 테스트하는 것은 정작 중요한 프로세스를 개발할 시간을 낭비하는 것과 같습니다. 무언가 프로세스가 변경되었을 때 바로바로 돌려보고 검증할 수 있는 테스트 코드는 필수적입니다.

최신 소스는 GitHub 사이트를 참고해 주세요.
https://github.com/codej99/SpringRestApi/tree/feature/junit-test

GitHub Repository를 import하여 Intellij 프로젝트를 구성하는 방법은 다음 포스팅을 참고해주세요.

Docker로 개발 환경을 빠르게 구축하는 것도 가능합니다. 다음 블로그 내용을 읽어보세요!

스프링 api 서버를 이용하여 웹사이트를 만들어보고 싶으시면 아래 포스팅을 참고해 주세요.

연재글 이동[이전글] SpringBoot2로 Rest api 만들기(8) – SpringSecurity 를 이용한 인증 및 권한부여
[다음글] SpringBoot2로 Rest api 만들기(10) – Social Login kakao