[Spring]Junit - 단위 테스트 해보기

Inung_92·2023년 7월 7일
1

Spring

목록 보기
12/15
post-thumbnail

포스팅 목적

📖Spring Boot에서 junit을 활용하여 각 계증별 단위 테스트를 수행하는 방법에 대해서 알아보자.

테스트의 이유

⚡️ 코드 결과 검증의 비용 측면

코드 결과를 검증하는 일반적인 과정을 살펴보자.

  • 코드를 작성한다.
  • 애플리케이션을 실행한다.
  • PostMan 혹은 개발된 브라우저의 Request 요청을 보낸다.
  • log / Println()을 통해 결과를 확인한다.
  • 결과에 대한 수정을 위해 애플리케이션을 종료한다.
  • 코드를 수정하고 위의 과정을 반복한다.

이러한 과정이 지속적으로 반복되면 애플리케이션의 규모에 따라 사용되는 검증의 비용이 늘어나게 된다. 그렇다면 이러한 부분을 해결하기 위해 테스트를 수행하면 어떻게 되는지 과정을 살펴보자.

  • 테스트 코드를 작성
  • 테스트 코드를 실행
  • 결과 검증
  • 코드 수정
  • 위의 내용 반복

테스트 과정은 애플리케이션을 직접 실행 및 종료 할 필요가 없다. 따라서 애플리케이션을 가동하는데 사용되는 비용 자체가 줄어들고 비교 결과까지 명확히 나오니 좋은 것이다.

⚡️ Spring의 구조적 측면

Spring의 구조를 살펴보면 다음과 같다.

애플리케이션을 직접 실행해서 결과를 검증한다면 어떤 계층에서 오류가 난 것인지 확인하는 것이 번거롭기도하고 복잡하다.

이런 부분을 단위 테스트를 통해 해결 할 수 있는 것이다. 해당 부분에 대한 기능만 확인하기 때문에 바로 그 위치의 코드마나 수정하면 된다.

테스트 분류

테스트는 크게 단위 테스트와 통합 테스트로 구분이 된다.

  • 단위 테스트 : 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 테스트(기능 또는 메소드)
  • 통합 테스트 : 모듈을 통합하는 과정에서 모듈 간의 호환성을 확인하는 테스트(여러 계층)

⚡️ 장단점

🖥️ 단위 테스트

  • 장점
    • 새로운 기능에 대한 빠른 코드 작성 및 검증 가능
    • 테스트 코드 자체가 하나의 문서
    • 시간과 비용 절약
  • 단점
    • 모듈간의 호환성 확인 제한
    • 가짜 객체 작성 필요(어쩌면 이 부분이 귀찮아서 안하는 거일지도...)
    • 실제 운영 환경과 상이한 결과를 가져올 수 있음

🖥️ 통합 테스트

  • 장점
    • 실제 객체를 사용하여 가짜 객체 정의 불필요
    • 실제 운영 환경에서의 테스트를 통한 검증 가능
  • 단점
    • 테스트 수행 시 비용이 많이 들어감
    • 오류 발생 시 계층 파악에 대한 시간 소요

이렇게 각 테스트는 장점과 단점이 명확하다. 하지만 오늘은 단위 테스트에 대한 설명을 할 예정이니 단위 테스트에 대해서 좀 더 알아보자.

⚡️ 올바른 단위 테스트

그렇다면 어떻게 단위 테스트를 하는게 올바른 방법인가?
아직 나도 테스트를 많이 수행해 본 상태가 아니기에 여러 블로그를 보며 읽은 부분을 나열해보겠다.

  • 단위 테스트 수행 시 1개의 기능에 대해서만 테스트
  • 테스트 주체와 협력자 구분
    • 주체 : 테스트를 수행 할 객체
    • 협력자 : 테스트를 진행하기 위해 필요한 가짜 객체
  • given, when, then 단계 준수
    • given : 테스트 사전 준비
    • when : 진행
    • then : 테스트 결과 검증

이러한 사항들에 유의하여 단위 테스트 코드를 작성해보도록 하겠다.

단위 테스트 수행

테스트 수행을 위해 사용 할 도구는 Junit5이다. Junit5에 대해서는 따로 자세히 다룰 예정이니 오늘 사용 할 어노테이션에 대해서만 확인하고 넘어가자.

⚡️ 어노테이션

구분설명
@Test테스트를 수행 할 메소드를 지정하며, void로 선언하여 반환값이 없어야함.
@DisplayName테스트명 지정, 지정하지 않을 시 메소드명으로 출력
@MockBean가짜 객체를 만드는 역할 수행
@BeforeEach테스트 수행 전 항상 실행되도록 설정(가짜 객체 주입 등에 사용)
@DataJdbcTestJDBC를 사용하는 Repository(DAO)에 대한 테스트 수행
@ExtendWith가짜 객체 사용 시 명시해야하는 어노테이션
@AutConfigureTestDatabase임베디드 된 DB를 사용하여 검증할지 여부를 판단, NONE 설정 시 실제 DB 활용
@WebMvcTestController 테스트 시 사용되는 어노테이션

테스트 수행 중 사용하는 라이브러리와 메소드가 있는데 해당 부분은 각 계층별 테스트를 설명하면서 설명하겠다.

⚡️ Domain 테스트

가장 단위가 작은 테스트이다. @Test 어노테이션은 필수이며 @DisplayName은 선택에 따라 적용하면 된다.

package com.demo.demoproject.domain;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class UserTest {

    @Test
    @DisplayName("유저 정보 설정 및 확인 테스트")
    void login(){
    	// given
        User user = new User();
		
        // when
        user.setUsr_id("test");
        user.setPwd("password");
		
        // then
        Assertions.assertThat(user.getUsr_id()).isEqualTo("eeee");
        Assertions.assertThat(user.getPwd()).isEqualTo("password");
    }
}

실행 결과는 다음과 같다.

user 객체의 usr_id는 "test" 설정하였는데 비교 값은 "eeee"이기 때문에 일치하지 않는 에러가 발생한 것이다. 올바른 값으로 비교를 하면 다음과 같이 출력된다.

여기서 사용된 객체 및 메소드에 대해서 알아보자.

  • Assertions : AssertJ 라이브러리에 포함된 객체이다. import는 아래 코드를 확인하자.
  • Assertions.assertThat() : 값 검증에 사용되는 메소드이다.
  • isEqualTo() : 인자로 기댓값을 넘기면 assertThat()의 인자 값과 비교한다.
import org.assertj.core.api.Assertions;

Assertions.assertThat(user.getUsr_id()).isEqualTo("eeee");
Assertions.assertThat(user.getPwd()).isEqualTo("password");

⚡️ Repository 테스트

DB와 연동이 되는 부분으로 실제 DB를 사용 할 수도 있고, 임베디드 DB를 사용 할 수도 있다. 나는 실제 DB를 사용하여 테스트한 방법에 대해서 설명하겠다.

package com.demo.demoproject.model.repository;

import com.demo.demoproject.domain.User;
import com.demo.demoproject.model.mapper.UserMapper;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.util.ReflectionTestUtils;


@DataJdbcTest
@ExtendWith(SpringExtension.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MybatisUserDAOTest {
	// 주체
    UserDAO userDAO;
	
    // 협력자(가짜객체)
    @MockBean
    UserMapper userMapper;
	
    // 가짜객체 주입
    @BeforeEach
    void setUserDAO(){
        userDAO = new MybatisUserDAO(userMapper);
    }

    @Test
    @DisplayName("유저 존재 여부 테스트")
    void loginUser(){
    	// given
    	// User 객체 생성
        User user = new User();
        user.setUsr_id("test");
        user.setPwd("test1234");
		
        // private로 선언된 필드 값 수정
        ReflectionTestUtils.setField(user, "usr_no", 31);
        
        // 가짜 객체의 로직을 수행하고 리턴값을 반환
        Mockito.when(userMapper.selectUser(user)).thenReturn(user);
		
        // when
        User result = userDAO.selectUser(user);
        System.out.println(result);
		
        // then
        Assertions.assertThat(result.getUsr_no()).isEqualTo(31);
    }

}

결과는 다음과 같다.

만약 여기서 isEqualTo(32);를 지정했다면 결과가 다음처럼 출력 될 것이다.

어노테이션들에 대해 간략히 정리하자.

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

SpringBoot 테스트에서 데이터베이스 설정을 변경하는데 사용된다. replace는 데이터베이스를 어떻게 구성할지에 대해 설정하는데 위 설정은 자동으로 구성하지 않고 애플리케이션의 데이터베이스 설정을 사용한다는 의미이다.

@DataJpaTest

Spring Data JPA와 관련된 설정을 자동으로 제공하는 어노테이션이다. @Entity와 Spring Data JPA Repository 인터페이스를 스캔해서 데이터베이스 계층의 빈을 설정하고 테스트한다. 테스트 수행 간 트랜잭션을 지원한다.

@ExtendWith(SpringExtension.class)

Junit5의 확장을 사용해서 Spring의 컨텍스트를 활성화하고 관련 기능을 테스트에 사용하도록 지원한다.

@SpringBootTest

Spring Boot 애플리케이션의 컨텍스트를 활성화한다. 애플리케이션을 시작하기 때문에 테스트가 느리며 무겁다. 통합 테스트에 사용되며 실제 환경과 유사한 테스트가 가능하다.

여기서 사용된 객체 및 메소드에 대해서 알아보자.

ReflectionTestUtils.setField(user, "usr_no", 31);

해당 코드는 User 객체의 private로 선언된 필드의 값을 세팅하는 코드이다. 위의 예시에서 user의 아이디와 비밀번호는 세팅을 해주었지만 번호는 세팅해주지 않았기 때문에 추가 세팅을 예시로 작성한 코드이다.

Mockito.when(userMapper.selectUser(user)).thenReturn(user);

해당 코드는 협력객체(가짜객체)인 userMapper의 로직을 수행하고 해당 로직이 수행되고나면 반환받을 값을 인자로 넘겨주는 코드이다.

이 외 코드는 Domain과 같다.

⚡️ Service 테스트

Service는 위의 Repository와 동일하므로 생략하겠다.

⚡️ Controller 테스트

Controller는 Http 메소드에 대한 요청을 받아들이는 객체이므로 @WebMvcTest 어노테이션을 명시해준다.

package com.demo.demoproject.controller;

import com.demo.demoproject.domain.User;
import com.demo.demoproject.model.service.UserServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONObject;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

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

@WebMvcTest
class LoginControllerTest {
    @Autowired
    MockMvc mvc;
    @Autowired![](https://velog.velcdn.com/images/ung6860/post/c77c9ae4-5f3d-41c5-935e-7182529e7620/image.png)

    ObjectMapper objectMapper;

    @MockBean
    UserServiceImpl userService;
    
    @Test
    @DisplayName("유저 로그인 요청")
    void login() throws Exception{
        User user = new User();
        user.setUsr_id("test");
        user.setPwd("test1234");

        Mockito.when(userService.login(user)).thenReturn(user);

        mvc.perform(MockMvcRequestBuilders.post("/login")
                        .content(objectMapper.writeValueAsString(user))
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.usr_id").value("test"))
                .andDo(MockMvcResultHandlers.print());
    }
}

위와 같이 실행하면 결과는 다음과 같다.

🖥️ request

🖥️ response

이렇게 MockHttpServlet 객체들을 이용하여 요청에 대한 응답을 확인 할 수 있다.

해당 테스트에서 사용된 객체 및 메소드를 살펴보자.

@Autowired
MockMvc mvc;

위 객체는 http 요청을 담당하는 객체이다.

mvc.perform(MockMvcRequestBuilders.post("/login")
		.content(objectMapper.writeValueAsString(user))
        .contentType(MediaType.APPLICATION_JSON))
	.andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath("$.usr_id").value("test"))
    .andDo(MockMvcResultHandlers.print());

메소드를 살펴보자.

  • perform() : http 메소드를 지정하여 실제 요청을 수행
  • andExpect() : 결과를 검증하는 기능을 수행하며 인자로 다양한 결과를 설정 가능
  • andDo() : 요청에 대한 처리 기능을 수행
  • $.usr_id.value() : 이 부분은 단일 객체일 경우 $., 복수 객체일 경우 $[index].usr_id 등으로 활용하여 사용하면되고, value()의 인자로는 비교 값을 넣어주면 된다.

마무리

여기까지 단위 테스트에 대한 간단한 사용 방법을 알아보았다.

아직 Junit과 AssertJ에 대한 부분도 더 공부를 해야하고, 다양한 테스트를 수행해봐야 더 알 수 있겠지만 확실한 건 테스트를 애플리케이션 수행없이 해 볼 수 있기에 소모되는 비용이 적은 것을 알 수 있다.

개발 공부를 시작하면서 계속 시도해보고 싶었던 테스트이지만 안해도 되겠지라는 안일한 마음에 미루다가 이제서야 사용한 것에 대한 아쉬움이 남는다.

그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글