현재 캡스톤디자인 프로젝트는 단 1개의 Test 코드도 작성하지 않고 개발하고 있었다. 😂
왜냐하면 기한까지 일단 기능 구현을 빨리해야되서 테스트코드를 짤 여유가 없었을 뿐더러 아직까지는 복잡한 로직을 구현한 코드가 없었기 때문에 테스트코드에 대한 필요성을 느끼지 못하고 있었다.
하지만 이곳 저곳에서 테스트 코드 작성이 개발의 절반을 차지할 정도로 중요하다는 말을 하고 나는 테스트코드의 적용의 필요성을 느꼈다..
고로 이번 포스팅에서는 캡스톤 디자인 프로젝트에 테스트 코드를 적용해보는 글을 써보겠다.
테스트 코드 작성 참고 사이트
https://dingdingmin-back-end-developer.tistory.com/entry/Springboot-Test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-1
- 검증 코드 작성
- 애플리케이션 실행
- PostMan 혹은 브라우저 Request 요청
- log 혹은 print로 결과 검증
- 원하지 않는 결과 발생 시 애플리케이션 종료
- 다시 코드 작성
- Test 코드 작성
- Test 코드 실행
- 결과 검증
- Test 코드 수정
위와 같이 Test 코드를 사용했을 때는 애플리케이션을 실행, 종료할 필요가 없어 비용이 줄어들고, Test 코드를 통해서 명확한 결과 검즈잉 가능하다.
애플리케이션을 실행해서 Test를 진행한다면, 어느 계층에서 잘못된 코드가 있는지 파악하는데 많은 비용이 든다. 하지만 Test코드를 통해서 계층별로 Test를 진행한다면 어느 부분이 잘못된지 파악을 쉽게할 수 있다.
Spring Initailizer를 통해서 프로젝트를 생성하면 spring-boot-starter-test dependency가 자동으로 추가된다. 우리는 이것을 이용해서 Test 코드를 작성하면 된다.
테스트의 종류와 적절한 선택은 개발 과정 및 애플리케이션의 특성에 따라 다르지만, 일반적으로 애플리케이션의 완전성을 보장하기 위해 단위 테스트와 통합 테스트 둘 다 수행하는 것이 가장 이상적이다. 이 두 종류의 테스트는 각각 다른 목적과 장단점을 가지고 있기 때문이다.
개별 코드 단위의 정확성을 보장하기 위해 단위 테스트를, 코드 간의 상호작용 및 전체 시스템의 동작을 검증하기 위해 통합 테스트를 수행하는 것이 좋다. 이 외에도 성능 테스트, 스트레스 테스트, 회귀 테스트, 스모크 테스트 등 여러 종류의 테스트가 있으며, 이들은 애플리케이션의 특성 및 요구 사항에 따라 선택하면 된다.
class UsersTest {
@Test
@DisplayName("유저가 생성되는지 확인하는 테스트")
void createUser() {
// given
Users users = Users.builder()
.loginId("test1234")
.encryptedPwd("encryptedPwd")
.name("kim")
.phoneNumber("010-1234-1234")
.birthday(LocalDate.of(2023,07,27))
.gender(MALE)
.profileImage("imageUrl")
.build();
// when, then
Assertions.assertThat(users.getName()).isEqualTo("kim");
Assertions.assertThat(users.getPhoneNumber()).isEqualTo("010-1234-1234");
}
}
@DataJpaTest: JPA를 사용하는 Repository에 대한 검증을 수행할 때 사용하는 어노테이션
💡 @DataJpaTest는 @Transaction을 포함하고 있어서 1개의 테스트가 끝나면 Rollback해 다른 테스트에게 영향을 미치지 않는다.
@DataJpaTest로 검증할 수 있는 목록
@DataJpaTest
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Test
@DisplayName("User 생성")
void createMember() {
// given
Users users = Users.builder()
.loginId("test1234")
.encryptedPwd("encryptedPwd")
.name("kim")
.phoneNumber("010-1234-1234")
.birthday(LocalDate.of(2023,07,27))
.gender(MALE)
.profileImage("imageUrl")
.build();
// when
Users result = userRepository.save(users);
// then
Assertions.assertThat(result.getName()).isEqualTo(users.getName());
}
@Test
@DisplayName("LoginId로 User 불러오기")
void findByLoginId() {
// given
Users users = Users.builder()
.loginId("test1234")
.encryptedPwd("encryptedPwd")
.name("kim")
.phoneNumber("010-1234-1234")
.birthday(LocalDate.of(2023,07,27))
.gender(MALE)
.profileImage("imageUrl")
.build();
userRepository.save(users);
// when
Users result = userRepository.findByLoginId("test1234");
// then
Assertions.assertThat(result.getName()).isEqualTo(users.getName());
}
}
Service 계층 같은 경우 UserRepository, ClubServiceClient, FileStore 객체를 Spring에게 주입받고 있다.
따라서 Service 계층의 Test는 주체가 Service 객체이며, 협력자는 UserRepository, ClubServiceClient, FileStore 객체이다.
그렇기에 UserRepository, ClubServiceClient, FileStore는 가짜 객체로서 응답을 설정해줘야한다.
JUnit5 기능을 사용하고, Test에서 가짜 객체를 사용하기 때문에 @ExtendWith(SpringExtension.class)를 붙여줘야 한다.
@ExtendWith(SpringExtension.class)
class UserServiceTest {
// Test 주체
UserService userService;
// Test 협력자
@MockBean
UserRepository userRepository;
@MockBean
ClubServiceClient clubServiceClient;
@MockBean
FileStore fileStore;
// Test를 실행하기 전마다 UserService에 가짜 객체를 주입시켜준다.
@BeforeEach
void setUp() {
userService = new UserServiceImpl(userRepository, clubServiceClient, fileStore);
}
@Test
@DisplayName("user-id로 User 조회")
void getUserById() {
// given
Users users = Users.builder()
.loginId("test1234")
.encryptedPwd("encryptedPwd")
.name("kim")
.phoneNumber("010-1234-1234")
.birthday(LocalDate.of(2023,07,27))
.gender(MALE)
.profileImage("imageUrl")
.build();
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(users));
// when, then
Assertions.assertThat(userService.getUserById(1L)).isInstanceOf(ResponseUserById.class);
}
}
Mockito
userRepository.findById(1L) 메서드가 호출되면 가짜 객체 응답으로 users를 반환하도록 설정할 수 있다.
즉, userRepository.findById(1L) 메서드를 호출할 때 실제 DB에 저장되는 대신 users 객체를 반환하도록 가짜로 설정한 것이다.
Mockito를 이용하여 userRepository의 동작을 가짜로 설정함으로써, 외부 종속성을 고려하지 않고 유저 조회 메서드만을 단독으로 테스트할 수 있다.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mvc;
@MockBean
UserServiceImpl userService;
@Test
@WithMockUser(username = "testUser", roles = "USER") // 가짜 사용자로 인증 시도
@DisplayName("회원 정보 조회")
void getUserById() throws Exception {
// given
Users users = Users.builder()
.loginId("test1234")
.encryptedPwd("encryptedPwd")
.name("kim")
.phoneNumber("010-1234-1234")
.birthday(LocalDate.of(2023,07,27))
.gender(MALE)
.profileImage("imageUrl")
.build();
ReflectionTestUtils.setField(users,"id",1L);
ResponseUserById response = ResponseUserById.builder().user(users).build();
Mockito.when(userService.getUserById(1L)).thenReturn(response);
// then
mvc.perform(MockMvcRequestBuilders.get("/user/{user-id}", 1L))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(response.getId()))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value(response.getName()));
}
}
ReflectionTestUtils.setField() : test를 진행하면서 private로 선언된 필드 값을 넣어줄 수 있다.
@WithMockUser(username = "testUser", roles = "USER"): 현재 프로젝트는 Spring Security를 사용하고 있어 사용자 인증이 필요하다.(인증을 안할 시 401 에러가 뜬다.) @WithMockUser를 사용하여 테스트 메소드에 인증된 사용자 정보를 추가하면, 해당 사용자로 인증된 것처럼 테스트를 수행할 수 있다.
MockMvc는 실제로 서블릿 컨테이너를 사용하지 않고, 테스트용으로 Mvc 기능을 사용할 수 있게 해주는 역할을 한다.
테스트 떄 생성되는 WebApplicationContext에서 주입받는다.
mvc.perform(MockMvcRequestBuilders.get(). contentType(): 컨트롤러에게 요청을 보내는 역할을 한다. uri를 만들고, contentType을 지정한다.
andDo(): 요청에 대한 처리를 한다. MockMvcResultHanlder.print()를 인자로 넣었으므로 요청과 응답에 대한 것들을 콘솔에 출력해준다.
andExpect(): 검증하는 로직. MockMvcResultMatcher.status()는 HTTP 상태 코드를 검증하고, jsonPath는 Json로 넘어온 것들에 대한 값을 검증할 수 있다.
이렇게 테스트 코드 작성의 이유와 간단한 테스트 코드 작성을 해봤다.
앞으로는 기능 구현을 할 때 테스트 코드를 작성할 수 있도록 해야겠다.