Blog

[Spring][258] 03 업무분담에 따른 CRUD작성과 테스트코드, 그리고 보완

Author
Summary
TEAM 258
Category
Project
Tags
Project
Favorite
Memory Date
2023/10/06
Cross Reference Study
Related Media
Related Thought
Related Lessons
tag
날짜
작성자
진행상황
진행 전
태그구분
6 more properties
💡
관리자 기능
우선 MVP에서는 관리자로 특정 회원들을 삭제하는 권한을 주려고 했다. 기본적으로 관리자 권한을 주려고 하는 것이고, 추후 부가 기능들에 관리자 권한이 많아 질 수 있다. 불량 회원 신고 등 기능을 추가하면 관리자가 경고를 보낸다던지 일시 정지 등 권한을 변경하는 기능을 추가 할 지 생각중이다. 현재는 우선 기본적인 회원 조회 기능과 회원 삭제 기능을 기본으로 구현해두었다.
💡
소스코드
MVP모델이라서 아직 크게 기능은 없는 상태이다. 추후 부가 기능 등을 추가하면서 관리자쪽의 기능은 추가 될 필요성이 있다. 우선 현재 상태의 최소 기능 소스코드는 다음과 같다.
AdminController.java
@Controller @RequestMapping("/api/admin") @RequiredArgsConstructor public class AdminController { private final AdminService adminService; @GetMapping public ResponseEntity<List<AdminResponseDto>> getAllUsers() { return ResponseEntity.ok(adminService.getAllUsers()); } @DeleteMapping("/{userId}") public ResponseEntity<MessageDto> deleteUser(@PathVariable Long userId, @AuthenticationPrincipal UserDetailsImpl userDetails) { return ResponseEntity.ok(adminService.deleteUser(userId, userDetails.getUser())); } }
Java
복사
AdminService.java
@Service @RequiredArgsConstructor public class AdminService { private final UserRepository userRepository; @Transactional(readOnly = true) public List<AdminResponseDto> getAllUsers() { return userRepository.findAll().stream().map(AdminResponseDto::new).toList(); } @Transactional public MessageDto deleteUser(Long userId, User loginUser) { User user = getUserById(userId); if (!loginUser.equals(user) && loginUser.getRole().equals(UserRoleEnum.ADMIN)) { throw new IllegalArgumentException("회원을 삭제할 권한이 없습니다."); } userRepository.delete(user); return new MessageDto("삭제가 완료되었습니다"); } private User getUserById(Long userId) { User user = userRepository.findById(userId) .orElseThrow(()-> new IllegalArgumentException("회원을 찾을 수 없습니다.")); return user; } }
Java
복사
AdminRepository.java
@Repository public interface AdminRepository extends JpaRepository<User, Long> { }
Java
복사
💡
Service 단위 테스트코드
주요 비지니스 로직이 담긴 AdminService.java의 테스트 코드를 작성했다. 처음에는 로그인을 생각하지 않고 단순히 기능적인 부분 코드만 작성하고, 실제 AdminService.java 코드를 작성했다. 그 이후 AdminService의 특성상 관리자 전용이라는 특수성 때문에라도 로그인 상태가 필수적이기 때문에 로그인을 필요로하는 테스트코드를 작성하고 그에 따라 본 AdminService.java를 수정했다. 테스트 코드를 먼저 작성하고 코드를 작성하는 방법이 생각보다 좀 더 효율적인것 같은 느낌이 들었다.
아직 본 Service코드가 복잡한 로직을 담고 있지 않기도 하지만 테스트 코드를 잘 작성하고 많은 케이스를 담아야 하는 것에 대한 개념이 부족한 것 같다.
@SpringBootTest class AdminServiceTest { @Mock private UserRepository userRepository; @InjectMocks private AdminService adminService; @Nested @DisplayName("AdminService - READ - 단순 로직 작동 여부 체크") class BasicReadTest { @Test @DisplayName("회원 전체 조회 성공") void Admin_getAllUsersSuccess() { // given User user1 = new User(); User user2 = new User(); User user3 = new User(); List<User> userList = Arrays.asList(user1, user2, user3); when(userRepository.findAll()).thenReturn(userList); // when List<AdminResponseDto> result = adminService.getAllUsers(); // then assertThat(result).hasSize(3); } @Test @DisplayName("회원 전체 조회 실패 - 등록된 회원이 없는 경우") void Admin_getAllUsersFail() { // given List<User> emptyUserList = Collections.emptyList(); when(userRepository.findAll()).thenReturn(emptyUserList); // when List<AdminResponseDto> result = adminService.getAllUsers(); // then assertThat(result).isEmpty(); } } @Nested @DisplayName("AdminService - DELETE - 단순 로직 작동 여부 체크") class BasicDeleteTest { @Test @DisplayName("유저 삭제 성공") void deleteUserSuccess() { // given User user = User.builder() .userId(1L) .username("user1") .password("pass1") .role(UserRoleEnum.USER) .build(); when(userRepository.findById(1L)).thenReturn(Optional.of(user)); doNothing().when(userRepository).delete(user); // when MessageDto result = adminService.deleteUser(1L, user); // then assertThat(result.getMsg()).isEqualTo("삭제가 완료되었습니다"); verify(userRepository, times(1)).delete(user); } @Test @DisplayName("유저 삭제 실패 - 존재하지 않는 유저 삭제 시도") void deleteNonExistingUserFail() { // given when(userRepository.findById(1L)).thenReturn(Optional.empty()); // when & then assertThrows(IllegalArgumentException.class, () -> adminService.deleteUser(1L, any(User.class)), "회원을 찾을 수 없습니다."); } } @Nested @DisplayName("AdminService - READ - 로그인된 회원의 ROLE에 대한 테스트") class LoginnedTest { @Test @DisplayName("회원 전체 조회 성공 - ADMIN으로 로그인한 경우에 성공") void Admin_getAllUsersSuccessWithAdminRoleSuccess() { // given User adminUser = User.builder() .userId(1L) .username("admin") .password("adminPass") .role(UserRoleEnum.valueOf("ADMIN")) .build(); List<User> userList = Arrays.asList(new User(), new User(), new User()); when(userRepository.findAll()).thenReturn(userList); // 가상의 관리자로 로그인 상태를 설정 SecurityContext securityContext = Mockito.mock(SecurityContext.class); Authentication authentication = Mockito.mock(Authentication.class); UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser); SecurityContextHolder.setContext(securityContext); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.getPrincipal()).thenReturn(adminUserDetails); // when List<AdminResponseDto> result = adminService.getAllUsers(); // then assertThat(result).hasSize(3); } @Test @DisplayName("회원 전체 조회 실패 - USER로 로그인한 경우에 조회 실패") void Admin_getAllUsersFailWithUserRole() { // given User user = User.builder() .userId(2L) .username("user") .password("userPass") .role(UserRoleEnum.valueOf("USER")) .build(); List<User> emptyUserList = Collections.emptyList(); when(userRepository.findAll()).thenReturn(emptyUserList); // 가상의 USER로 로그인 상태를 설정 SecurityContext securityContext = Mockito.mock(SecurityContext.class); Authentication authentication = Mockito.mock(Authentication.class); UserDetailsImpl userDetails = new UserDetailsImpl(user); SecurityContextHolder.setContext(securityContext); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.getPrincipal()).thenReturn(userDetails); // when List<AdminResponseDto> result = adminService.getAllUsers(); // then assertThat(result).isEmpty(); } } @Nested @DisplayName("AdminService - DELETE - 로그인된 회원의 ROLE에 대한 테스트") class LoginnedDeleteTest { @Test @DisplayName("유저 삭제 성공 - 관리자로 로그인한 경우") void deleteUserSuccessWithAdminRole() { // given User adminUser = User.builder() .userId(1L) .username("admin") .password("adminPass") .role(UserRoleEnum.ADMIN) .build(); User userToDelete = User.builder() .userId(3L) .username("userToDelete") .password("password") .role(UserRoleEnum.USER) .build(); when(userRepository.findById(3L)).thenReturn(Optional.of(userToDelete)); // 가상의 ADMIN으로 로그인 상태를 설정 SecurityContext securityContext = Mockito.mock(SecurityContext.class); Authentication authentication = Mockito.mock(Authentication.class); UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser); SecurityContextHolder.setContext(securityContext); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.getPrincipal()).thenReturn(adminUserDetails); // when MessageDto msg = adminService.deleteUser(3L, userToDelete); // then assertThat(msg.getMsg()).isEqualTo("삭제가 완료되었습니다"); } @Test @DisplayName("유저 삭제 실패 - 존재하지 않는 유저 삭제 시도") void deleteNonExistingUserFail() { // given User adminUser = User.builder() .userId(1L) .username("admin") .password("adminPass") .role(UserRoleEnum.ADMIN) .build(); when(userRepository.findById(3L)).thenReturn(Optional.empty()); // 가상의 관리자로 로그인 상태를 설정 SecurityContext securityContext = Mockito.mock(SecurityContext.class); Authentication authentication = Mockito.mock(Authentication.class); UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser); SecurityContextHolder.setContext(securityContext); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.getPrincipal()).thenReturn(adminUserDetails); // when & then assertThrows(IllegalArgumentException.class, () -> adminService.deleteUser(3L, any(User.class)), "회원을 찾을 수 없습니다."); } } }
Java
복사
💡
Controller 단위 테스트 코드
요청으로부터 정상적으로 요청에 따라 Controller가 작동하는지 테스트 코드이다. 이것도 Service 단위 테스트와 마찬가지로 Mockito 모의 객체를 활용해서 AdminController의 작동 부분에 대한 테스트를 생각해서 작성해보았다. 이것도 마찬가지로 첫 단순 로직 테스트를 먼저 작성해보고, 그에 따라 실제 AdminController.java를 구성, 이후 로그인을 고려하여 다시 발전시키는 방법으로 해보았다.
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @SpringBootTest @AutoConfigureTestDatabase @AutoConfigureMockMvc(addFilters = false) class AdminControllerTest { @Autowired private MockMvc mockMvc; @MockBean private AdminService adminService; @Autowired private final ObjectMapper objectMapper = new ObjectMapper(); User user1 = User.builder() .userId(1L) .username("user1") .password("1234") .role(UserRoleEnum.USER) .build(); User user2 = User.builder() .userId(2L) .username("user2") .password("1234") .role(UserRoleEnum.USER) .build(); User adminUser = User.builder() .userId(3L) .username("admin") .password("1234") .role(UserRoleEnum.ADMIN) .build(); UserDetails authenticateUser(User user) { UserDetailsImpl userDetails = new UserDetailsImpl(user); Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); return userDetails; } @Nested @DisplayName("AdminController - 단순 로직 작동 여부 테스트") class BasicControllerReadTest { @Test @DisplayName("단순_회원조회_테스트") void 단순_회원조회_테스트() throws Exception { // given List<AdminResponseDto> userList = new ArrayList<>(); userList.add(new AdminResponseDto(user1)); userList.add(new AdminResponseDto(user2)); userList.add(new AdminResponseDto(adminUser)); // when when(adminService.getAllUsers()) .thenReturn(userList); // then mockMvc.perform(get("/api/admin")) .andExpect(status().isOk()) .andDo(print()); } @Test @DisplayName("단순_회원삭제_테스트") void 단순_회원삭제_테스트() throws Exception { // given MessageDto msg = MessageDto.builder() .msg("회원 삭제 성공") .build(); // when when(adminService.deleteUser(user1.getUserId(), user1)).thenReturn(msg); // then mockMvc.perform(delete("/api/admin/{userId}", user1.getUserId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(authenticateUser(user1)))) .andExpect(status().isOk()) .andExpect(jsonPath("$.msg").value("회원 삭제 성공")); } } @Nested @DisplayName("AdminController - 로그인된 회원의 ROLE에 대한 테스트") class LoginnedControllerTest { @Test @DisplayName("회원 조회 성공 - ADMIN으로 로그인한 경우에 성공") void 관리자_회원목록조회_테스트() throws Exception { // given List<AdminResponseDto> userList = new ArrayList<>(); userList.add(new AdminResponseDto(user1)); userList.add(new AdminResponseDto(user2)); userList.add(new AdminResponseDto(adminUser)); // 가상의 ADMIN으로 로그인 상태를 설정 UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser); Authentication authentication = new UsernamePasswordAuthenticationToken(adminUserDetails, null, adminUserDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); // when when(adminService.getAllUsers()) .thenReturn(userList); // then mockMvc.perform(get("/api/admin")) .andExpect(status().isOk()) .andDo(print()); } @Test @DisplayName("회원 삭제 성공 - ADMIN으로 로그인한 경우에 성공") void 관리자_회원삭제_테스트() throws Exception { // given MessageDto msg = MessageDto.builder() .msg("관리자 권한 회원 삭제 성공") .build(); // 가상의 ADMIN으로 로그인 상태를 설정 UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser); Authentication authentication = new UsernamePasswordAuthenticationToken(adminUserDetails, null, adminUserDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); // when when(adminService.deleteUser(user1.getUserId(), adminUser)).thenReturn(msg); // then mockMvc.perform(delete("/api/admin/{userId}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(authenticateUser(adminUser)))) .andExpect(status().isOk()) .andExpect(jsonPath("$.msg").value("관리자 권한 회원 삭제 성공")); } } }
Java
복사