📖Spring Boot에서 junit을 활용하여 각 계증별 단위 테스트를 수행하는 방법에 대해서 알아보자.
코드 결과를 검증하는 일반적인 과정을 살펴보자.
이러한 과정이 지속적으로 반복되면 애플리케이션의 규모에 따라 사용되는 검증의 비용이 늘어나게 된다. 그렇다면 이러한 부분을 해결하기 위해 테스트를 수행하면 어떻게 되는지 과정을 살펴보자.
테스트 과정은 애플리케이션을 직접 실행 및 종료 할 필요가 없다. 따라서 애플리케이션을 가동하는데 사용되는 비용 자체가 줄어들고 비교 결과까지 명확히 나오니 좋은 것이다.
Spring의 구조를 살펴보면 다음과 같다.
애플리케이션을 직접 실행해서 결과를 검증한다면 어떤 계층에서 오류가 난 것인지 확인하는 것이 번거롭기도하고 복잡하다.
이런 부분을 단위 테스트를 통해 해결 할 수 있는 것이다. 해당 부분에 대한 기능만 확인하기 때문에 바로 그 위치의 코드마나 수정하면 된다.
테스트는 크게 단위 테스트와 통합 테스트로 구분이 된다.
이렇게 각 테스트는 장점과 단점이 명확하다. 하지만 오늘은 단위 테스트에 대한 설명을 할 예정이니 단위 테스트에 대해서 좀 더 알아보자.
그렇다면 어떻게 단위 테스트를 하는게 올바른 방법인가?
아직 나도 테스트를 많이 수행해 본 상태가 아니기에 여러 블로그를 보며 읽은 부분을 나열해보겠다.
이러한 사항들에 유의하여 단위 테스트 코드를 작성해보도록 하겠다.
테스트 수행을 위해 사용 할 도구는 Junit5이다. Junit5에 대해서는 따로 자세히 다룰 예정이니 오늘 사용 할 어노테이션에 대해서만 확인하고 넘어가자.
구분 | 설명 |
---|---|
@Test | 테스트를 수행 할 메소드를 지정하며, void로 선언하여 반환값이 없어야함. |
@DisplayName | 테스트명 지정, 지정하지 않을 시 메소드명으로 출력 |
@MockBean | 가짜 객체를 만드는 역할 수행 |
@BeforeEach | 테스트 수행 전 항상 실행되도록 설정(가짜 객체 주입 등에 사용) |
@DataJdbcTest | JDBC를 사용하는 Repository(DAO)에 대한 테스트 수행 |
@ExtendWith | 가짜 객체 사용 시 명시해야하는 어노테이션 |
@AutConfigureTestDatabase | 임베디드 된 DB를 사용하여 검증할지 여부를 판단, NONE 설정 시 실제 DB 활용 |
@WebMvcTest | Controller 테스트 시 사용되는 어노테이션 |
테스트 수행 중 사용하는 라이브러리와 메소드가 있는데 해당 부분은 각 계층별 테스트를 설명하면서 설명하겠다.
가장 단위가 작은 테스트이다. @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"이기 때문에 일치하지 않는 에러가 발생한 것이다. 올바른 값으로 비교를 하면 다음과 같이 출력된다.
여기서 사용된 객체 및 메소드에 대해서 알아보자.
import org.assertj.core.api.Assertions;
Assertions.assertThat(user.getUsr_id()).isEqualTo("eeee");
Assertions.assertThat(user.getPwd()).isEqualTo("password");
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는 위의 Repository와 동일하므로 생략하겠다.
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());
}
}
위와 같이 실행하면 결과는 다음과 같다.
이렇게 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());
메소드를 살펴보자.
여기까지 단위 테스트에 대한 간단한 사용 방법을 알아보았다.
아직 Junit과 AssertJ에 대한 부분도 더 공부를 해야하고, 다양한 테스트를 수행해봐야 더 알 수 있겠지만 확실한 건 테스트를 애플리케이션 수행없이 해 볼 수 있기에 소모되는 비용이 적은 것을 알 수 있다.
개발 공부를 시작하면서 계속 시도해보고 싶었던 테스트이지만 안해도 되겠지라는 안일한 마음에 미루다가 이제서야 사용한 것에 대한 아쉬움이 남는다.
그럼 이만.👊🏽