Testing
💡 테스팅이란?
저희가 만든 산출물이 기대한 대로 작동하는지를 시험해 보는 것을 .
어느 단계의 산출물을 테스트 하는지에 따라서 계층으로 나누어서 생각할 수 있다.
✅ 테스트의 종류
- 단위 테스트Unit Test : 개별 코드 단위(주로 메소드)를 테스트 하는 단계
→ 컨트롤러, 서비스, 레포지토리 계층에서 정의한 개별 메소드들이 정상적으로 작동하는지 테스트 하는 것을 의미합니다.
- 통합 테스트 Integration Test : 서로 다른 모듈이 상호 작용 하는 것을 테스트 하는 단계
→ 컨트롤러, 서비스, 레포지토리가 전체 그림에서 유연하게 상효작용하는지를 테스트하는 것을 의미합니다.
- 시스템 테스트 System Test : 완전히 통합되어 구축된 시스템을 테스트 하는 단계
✅ 테스트 코드의 장점
- 잘못된 방향의 개발을 막는다.
- 전체적인 코드의 품질이 상승한다.
- 최종적으로는 오류 상황에 대한 대처가 좋아져서 전체적인 개발 시간이 줄어든다.
✅ 테스트 코드의 단점
- 테스트 코드를 작성함으로써 개발 시간이 늘어난다.
- 테스크코드도 유지보수가 필요해서 유지보수 비용도 늘어난다.
- 테스트 작성법을 따로 배워야한다.
💡 H2
초기 단계의 개발 및 테스트에서 많이 활용하는, 메모리에서 동작하는 관계형 데이터베이
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Repository, Service 단위 테스트
Repository 단위 테스트 : 테스트를 관리하기 위한 클래스 작성
@DataJpaTest
public class UserRepositoryTests {
@Autowired
private UserRepository userRepository;
}
@DataJpaTest : JPA 기능만 테스트 하기 위한 어노테이션
@Autowired : 의존성 주입 어노테이션
💡 given – when – then 패턴
: 테스트를 가독성이 좋게 작성하기 위한 패턴
✅ given : 테스트가 진행되기 위한 전제조건을 준비하는 구간
// 새로운 UserEntity 준비
String username = "daeon.dev";
UserEntity user = new UserEntity();
user.setUsername(username);
✅ when : 테스트 하고 싶은 실제 기능을 작성하는 구간
// when; 테스트 하고 싶은 실제 기능을 작성하는 구간
user = userRepository.save(user);
✅ then : when에서 받은 실행한 결과가 기대한 대로 반환되었는 검증하는 구간
// then; 실행한 결과가 기대한 것과 같은지를 검증하는 구간
// 1. 새로 반환받은 user의 id는 null이 아님
assertNotNull(user.getId());
// 2. 새로 반환받은 user의 username은 우리가 넣었던 username과 일치
// 동일
assertEquals(username, user.getUsername());
- assertNotNull : 주어진 값이 null 이 아닌지를 검증합니다.
- assertEquals : 주어진 두 값이 동일한지를 검증합니다
여기서 제시되는 테스트 이름은, @DisplayName 어노테이션을 통해 정할 수 있습니다.
새로운 UserEntity 생성 실패 테스트
- given: 미리 username 을 가진 UserEntity를 생성해 둡니다.
- when: 동일한 username 을 가진 UserEntity 의 생성을 시도합니다.
- then: 예외가 발생합니다
@Test
@DisplayName("새 UserEntity를 데이터 베이스에 추가 실패")
public void testSaveNewFail() {
//given
String username = "daeon.dev";
UserEntity userGiven = new UserEntity();
userGiven.setUsername(username);
userRepository.save(userGiven);
//when
UserEntity user = new UserEntity();
user.setUsername(username);
//when-then
assertThrows(Exception.class, () -> userRepository.save(user));
}
assertThrows 는 전달받은 메소드를 실행하면서, 그 과정에서 예외가 발생했는지를 검증하는 용도로 사용됩니다
그 외 테스트
UserService에서 UserRepository를 다양한 방법으로 사용하는 만큼, 그에 해당하는 다양한 기능을 테스트로 작성해 봅시다.
- username으로 UserEntity 찾기
@Test
@DisplayName("username으로 UserEntity 찾기")
public void testFindByUsername() {
// given: 검색할 UserEntity 미리 생성
String username = "jeeho.dev";
UserEntity userGiven = new UserEntity();
userGiven.setUsername(username);
userRepository.save(userGiven);
// when: userRepository.findByUsername()
Optional<UserEntity> optionalUser
= userRepository.findByUsername(username);
// then: Optional.isPresent(), username == username
assertTrue(optionalUser.isPresent());
assertEquals(username, optionalUser.get().getUsername());
}
// username으로 찾기 실패
// username으로 존재하는지 확인
}
UserRepositoryTests 전체코드
package com.example.contents;
import com.example.contents.entity.UserEntity;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
public class UserRepositoryTests {
@Autowired
private UserRepository userRepository;
//실제 진행하고자하는 코드 작성하면 됨
// 새 UserEntity를 데이터 베이스에 추가 성공
@Test
@DisplayName("새 UserEntity를 데이터 베이스에 추가 성공")
public void testSaveNew() {
// given; 테스트가 진행되기 위한 전제조건을 준비하는 구간
// 새로운 UserEntity 준비
String username = "daeon.dev";
UserEntity user = new UserEntity();
user.setUsername(username);
// when; 테스트 하고 싶은 실제 기능을 작성하는 구간
user = userRepository.save(user);
// then; 실행한 결과가 기대한 것과 같은지를 검증하는 구간
// 1. 새로 반환받은 user의 id는 null이 아님
assertNotNull(user.getId());
// 2. 새로 반환받은 user의 username은 우리가 넣었던 username과 일치
// 동일
assertEquals(username, user.getUsername());
}
@Test
@DisplayName("새 UserEntity를 데이터 베이스에 추가 실패")
public void testSaveNewFail() {
//given
String username = "daeon.dev";
UserEntity userGiven = new UserEntity();
userGiven.setUsername(username);
userRepository.save(userGiven);
//when
UserEntity user = new UserEntity();
user.setUsername(username);
//when-then
assertThrows(Exception.class, () -> userRepository.save(user));
}
@Test
@DisplayName("username으로 UserEntity 찾기")
public void testFindByUsername() {
// given: 검색할 UserEntity 미리 생성
String username = "jeeho.dev";
UserEntity userGiven = new UserEntity();
userGiven.setUsername(username);
userRepository.save(userGiven);
// when: userRepository.findByUsername()
Optional<UserEntity> optionalUser
= userRepository.findByUsername(username);
// then: Optional.isPresent(), username == username
assertTrue(optionalUser.isPresent());
assertEquals(username, optionalUser.get().getUsername());
}
// username으로 찾기 실패
// username으로 존재하는지 확인
}
Service 단위 테스트
- UserServiceTest
- UserService는 UserRepository를 필요로 한다
- 단위 테스트는 하나의 클래스를 격리해서 테스트 하는것을 목표로 하는 만큼, 다른 단위에서 테스트 해야되는 repository의 기능에 의존해서는 안됩니다. 이런상황에서 단위 테스트를 하기 위해서 UserRepository의 역할을 따라하는 임시객체를 만들어서 사용하는데 이 임시 객체를 Mock(모조)라고 합니다.
💡Mock 객체: Repository의 기능을 흉내 내는 모조 객체
@Mock :이 객체는 모조품, 즉 Mock 객체임을 나타내는 어노테이션 입니다.
@injectMocks : 이 객체가 필요로 하는 의존성을 정의한 Mock 객체로 전달한다는 의미의 어노테이션 입니다.
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
}
- UserDto를 받아 UserEntity 생성
@Test
@DisplayName("UserDto로 createUser")
public void testCreateUser(){
//given
//1.userRepository 전달받을 userEntity정의
String username = "daeon.dev";
UserEntity userEntityIn = new UserEntity();
userEntityIn.setUsername(username);
//2. userRepository 반환할 userEntity정의 정의
Long userId = 1L;
UserEntity userEntityOut = new UserEntity();
userEntityOut.setId(userId);
userEntityOut.setUsername(username);
//3, userRepository.save()의 기능을 따라하도록 설정
// = userEntityIn을 저장하게 되면, userEntityOut를 반환하게끔 설정
when(userRepository.save(userEntityIn))
.thenReturn(userEntityOut);
when(userRepository.existsByUsername(username))
.thenReturn(false);
//when
UserDto userDto = new UserDto();
userDto.setUsername(username);
UserDto result = userService.createUser(userDto);
//then
assertEquals(userId, result.getId());
assertEquals(username, result.getUsername());
}
UserServiceTest 전체코드
package com.example.contents;
import com.example.contents.dto.UserDto;
import com.example.contents.entity.UserEntity;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
// UserDto(id!=null)를 입력받아 UserDto(id!=null)을 반환
@Test
@DisplayName("UserDto로 createUser")
public void testCreateUser(){
//given
//1.userRepository 전달받을 userEntity정의
String username = "daeon.dev";
UserEntity userEntityIn = new UserEntity();
userEntityIn.setUsername(username);
//2. userRepository 반환할 userEntity정의 정의
Long userId = 1L;
UserEntity userEntityOut = new UserEntity();
userEntityOut.setId(userId);
userEntityOut.setUsername(username);
//3, userRepository.save()의 기능을 따라하도록 설정
// = userEntityIn을 저장하게 되면, userEntityOut를 반환하게끔 설정
when(userRepository.save(userEntityIn))
.thenReturn(userEntityOut);
when(userRepository.existsByUsername(username))
.thenReturn(false);
//when
UserDto userDto = new UserDto();
userDto.setUsername(username);
UserDto result = userService.createUser(userDto);
//then
assertEquals(userId, result.getId());
assertEquals(username, result.getUsername());
}
}
@ExtendWith : Mock 객체를 만들기 위해서 Mockito를 사용한다는 부분을 첨부한 어노테이션
Controller 단위, 통합 테스트
- UserControllerTest
- 테스트 준비
//UserService 만들기 Mocking
@ExtendWith(MockitoExtension.class)
public class UserControllerTest {
@Mock
private UserService userService;
@InjectMocks
private UserController userController;
//Controller가 있을 때 HTTP 요청이 보내졌다 가정해주는 객체
private MockMvc mockMvc;
@BeforeEach
public void beforeEach(){
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
}
MockMvc는 MockMvcBuilder.standalonSetup(userController).build() 를 사용하면 UserController 를 테스트 하기 위한 엔드포인트만 설정한 서버를 Mock 합니다.
그리고 이를 실행하는 @BeforeEach 어노테이션이 붙은 메소드는 각 단위 테스트 이전에 mockMvc 가 초기화 되게 합니다.
그 외 UserService 의 @Mock 과 UserController의 @InjectMocks 는 이전과 동일하게 동작합니다.
2. 테스트 코드 수정
- UserControllerTest 전체코드
package com.example.contents;
import com.example.contents.dto.UserDto;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
//UserService 만들기 Mocking
@ExtendWith(MockitoExtension.class)
public class UserControllerTest {
@Mock
private UserService userService;
@InjectMocks
private UserController userController;
// Controller가 있을때 HTTP 요청이 보내졌다 가정해주는 객체
private MockMvc mockMvc;
@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
@Test
@DisplayName("UserDto를 나타내는 JSON 요청을 보내면 id가 null이 아닌 UserDto JSON 응답")
public void testCreate() throws Exception {
// given
// 1. userService.createUser 에 전달한 UserDto 준비
String username = "jeeho.dev";
UserDto requestDto = new UserDto();
requestDto.setUsername(username);
// 2. userService.createUser 가 반환할 UserDto 준비
Long userId = 1L;
UserDto responseDto = new UserDto();
responseDto.setUsername(requestDto.getUsername());
responseDto.setId(userId);
// 3. userService.createUser 의 동작 가정
when(userService.createUser(requestDto))
.thenReturn(responseDto);
// when
// perform: HTTP 요청을 보낸것을 시뮬레이션 하여 UserController 에게
ResultActions result = mockMvc.perform(
// 요청의 형태(Body 라던지)를 빌더처럼 정의
post("/users")
.content(JsonUtil.toJson(requestDto))
.contentType(MediaType.APPLICATION_JSON));
// then
result.andExpectAll(
status().is2xxSuccessful(), // 상태코드가 200
content().contentType(MediaType.APPLICATION_JSON), // 응답이 JSON 형태로
jsonPath("$.username", is(username)), // username은 요청한 값 그대로
jsonPath("$.id", notNullValue()) // id는 null은 아닌 값
);
mockMvc.perform(
// 요청의 형태(Body 라던지)를 빌더처럼 정의
post("/users")
.content(JsonUtil.toJson(requestDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpectAll(
status().is2xxSuccessful(), // 상태코드가 200
content().contentType(MediaType.APPLICATION_JSON), // 응답이 JSON 형태로
jsonPath("$.username", is(username)), // username은 요청한 값 그대로
jsonPath("$.id", notNullValue()) // id는 null은 아닌 값
);
}
}
'Spring' 카테고리의 다른 글
[Spring Security] JWT (0) | 2023.07.10 |
---|---|
[Spring] Logging (0) | 2023.07.09 |
[Spring] 유효성 검사 (0) | 2023.07.09 |
POSTMAN 사용하기 (0) | 2023.07.09 |
MyBatis (0) | 2023.07.09 |