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
복사