해당 프로젝트에서는 테스트 단위를 넓게 잡는 방식으로 통합 테스트를 진행하였습니다.
MockMvc
를 이용하여 컨트롤러에 HTTP 요청을 보낸 후 응답을 받는 형식으로 테스트를 진행하였고 필요한 경우에 @MockBean
을 이용하여 Mock 객체를 정의하여 사용하였습니다.
자세한 내용은 아래 테스트마다 코드를 보면서 설명드리도록 하겠습니다.
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
class MemberTest {
@Autowired
MemberRepository memberRepository;
@Autowired
MemberProfileRepository memberProfileRepository;
@Autowired
MemberDetailRepository memberDetailRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
private ObjectMapper objectMapper;
@Autowired
FileRepository fileRepository;
@Autowired
FileService fileService;
@MockBean
RedisService redisService;
private static String token = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIyIiwiZXhwIjoxNzE2NjE5MjAyfQ.K1z5xJJsthPZI3BUqSjbs8sa-l7gwaNVAc5NO_CoPtSl-cs18o67J4UpWykqnA-Q0NerEYt9vM7mM1tbzCdcuQ";
private static String refreshTokenKey = "refresh:SMKUt25uIr6opwVA7FbDyZGEYSQzUILOp3LLtXskm36c90/3sOMVGV0w62ReY3u1MjsKZkgZi0E7kTmks7/joA==_2";
@Autowired
MockMvc mockMvc;
테스트를 진행할 레포지토리들과 서비스들, Json 형태의 데이터와 자바 객체를 변환해주기 위해 ObjectMapper
를 의존성 주입해주었습니다.
Redis에서 토큰을 관리하고 있는데 실제로 Redis와의 통신을 하는 대신 Mock 객체로 설정하여 테스트를 진행하였습니다.
@SpringBootTest
를 사용하여 애플리케이션의 전체 스프링 컨텍스트를 로드하고, @AutoConfigureMockMvc
를 사용하여 MockMvc 인스턴스를 자동으로 구성합니다.
이렇게 구성된 MockMvc는 애플리케이션에서 활성화된 모든 필터와 보안 설정을 반영하여 HTTP 요청을 처리합니다.
@DisplayName("회원 권한 테스트")
@Test
public void memberAuthorityTest() throws Exception {
// given
Role userRole = roleRepository.findByRoleName(Role.RoleName.USER).get();
Role adminRole = roleRepository.findByRoleName(Role.RoleName.ADMIN).get();
Member testMember = memberRepository.findById(2L).get();
// when
String originalRole = testMember.getRole().getRoleName().name();
// 기존 회원의 권한은 일반 사용자입니다.
assertEquals(originalRole, "USER");
//then
// 일반 사용자의 경우 /admin에 대한 요청에 403 권한 에러가 발생하여야 합니다.
mockMvc.perform(get("/admin/test1").header("Authorization", token))
.andExpect(status().isForbidden());
// when
// 기존 회원의 권한을 관리자로 바꿔봅니다.
testMember.changeRole(adminRole);
// then
// /admin에 대한 요청 시 관리자로 권한을 바꿨기 때문에 통과되어야 합니다.
mockMvc.perform(get("/admin/test1").header("Authorization", token))
.andExpect((status().isOk()));
}
회원 권한 테스트의 경우 관리자일 경우와 일반 사용자일 경우에 대해 스프링 시큐리티 권한 검증에 대한 검사를 진행하였습니다.
실제 DB에 저장된 기존 유저(사용자 권한을 가진)를 꺼내온 후 mockMvc
를 이용하여 관리자 권한일 경우에 통과하고 일반 사용자 권한일 경우에 403 에러가 발생하는지를 테스트하였습니다.
mockMvc
설정에서 요청에 JWT 토큰를 헤더에 담아서 요청을 보내고 응답 상태를 체크하도록 하였습니다.
@DisplayName("회원 프로필 조회 테스트")
@Test
public void getMemberProfileTest() throws Exception {
// given
// when
// pk가 2L인 사용자의 프로필을 조회합니다. 헤더의 토큰은 해당 pk의 사용자 토큰입니다.
List<MemberProfileResponseDto> memberProfiles = memberProfileRepository.findMemberProfiles(2L, false);
MemberProfileResponseDto memberProfile = memberProfiles.get(0);
// then
// jsonPath를 이용하여 컨트롤러가 주는 HTTP 응답 바디에 접근하여 값을 비교하는 코드입니다.
mockMvc.perform(get("/members")
.header("Authorization", token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].memberId").value(memberProfile.getMemberId()))
.andExpect(jsonPath("$.content[0].nickName").value(memberProfile.getNickName()))
.andExpect(jsonPath("$.content[0].description").value(memberProfile.getDescription()))
.andExpect(jsonPath("$.content[0].createdAt").value(memberProfile.getCreatedAt()))
.andExpect(jsonPath("$.content[0].updatedAt").value(memberProfile.getUpdatedAt()))
.andExpect(jsonPath("$.content[0].profileImage").value(memberProfile.getProfileImage()));
}
회원 프로필 조회 테스트에서는 실제 DB에서 회원 프로필을 조회한 후 mockMvc
를 이용하여 HTTP 요청을 보낸 후 받은 응답값과 비교하여 검사를 진행하고 있습니다.
MockMvcResultMatchers 클래스의 jsonPath를 이용하여 컨트롤러가 응답으로 준 JSON 데이터의 값을 점표기법을 통해 속성의 값을 추출하여 실제 DB에서 가져온 객체의 속성 값과 비교하였습니다.
@DisplayName("회원 프로필 수정 BindingResult 검증 테스트")
@Test
public void modifyMemberProfileValidTest() throws Exception {
// given
MemberProfilePatchDto inAppropriateNickname = new MemberProfilePatchDto("닉네임", "테스트를 위한 프로필 수정입니다. 참고해주시면 감사하겠습니다", 1L, null);
MemberProfilePatchDto inAppropriateDescription = new MemberProfilePatchDto("닉네임테스트", "부적절한설명", 1L, null);
// 요청 메시지 바디에 JSON 형태로 넣어주기 위해 객체 직렬화 합니다.
String inAppropriateNicknameJson = objectMapper.writeValueAsString(inAppropriateNickname);
String inAppropriateDescriptionJson = objectMapper.writeValueAsString(inAppropriateDescription);
// when & then
// 잘못된 형식의 닉네임을 넣어줬을 때 제대로 BindingResult의 유효성 검사를 진행하는지 테스트합니다.
mockMvc.perform(patch("/members/profile")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(inAppropriateNicknameJson))
.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.errorResponse.errorName").value("INCORRECT_FORMAT_NICKNAME"));
// 잘못된 형식의 description을 넣어줬을 때 제대로 BindingResult의 유효성 검사를 진행하는지 테스트합니다.
mockMvc.perform(patch("/members/profile")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(inAppropriateDescriptionJson))
.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.errorResponse.errorName").value("INCORRECT_FORMAT_DESCRIPTION"));
}
회원 프로필 수정 BindingResult 검증 테스트에서는 Nickname, Description에 대한 유효성 검사를 진행합니다.
잘못된 데이터가 들어있는 DTO를 생성한 후 mockMvc.contentType
을 MediaType.APPLICATION_JSON으로, mockMvc.content
에 DTO를 넣어 요청을 생성해주었습니다.
각각의 조건에 맞는 예외를 내뱉는지 확인을 해주었습니다.
@DisplayName("회원 프로필 수정 Invalid fileId 테스트")
@Test
public void modifyMemberProfileFailTest() throws Exception {
// given
MemberProfilePatchDto invalidFileInProfile = new MemberProfilePatchDto("수정할닉네임", "테스트를 위한 프로필 수정입니다. 참고해주시면 감사하겠습니다", 100L, null);
// 요청 메시지 바디에 JSON 형태로 넣어주기 위해 객체 직렬화 합니다.
String invalidFileInProfileJson = objectMapper.writeValueAsString(invalidFileInProfile);
// when & then
// 잘못된 형식의 닉네임을 넣어줬을 때 제대로 BindingResult의 유효성 검사를 진행하는지 테스트합니다.
mockMvc.perform(patch("/members/profile")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(invalidFileInProfileJson))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorResponse.errorName").value("FILE_NOT_FOUND"));
}
해당 테스트는 위의 테스트와 비슷한 방식으로 진행하였기에 설명을 생략하도록 하겠습니다.
@DisplayName("회원 프로필 수정 정상 동작 테스트")
@Test
public void modifyMemberProfileTest() throws Exception {
// given
MemberProfilePatchDto profile = new MemberProfilePatchDto("수정할닉네임", "테스트를 위한 프로필 수정입니다. 참고해주시면 감사하겠습니다", 1L, null);
String profileImage = fileRepository.findById(1L).get().getPath();
// 요청 메시지 바디에 JSON 형태로 넣어주기 위해 객체 직렬화 합니다.
String profileJson = objectMapper.writeValueAsString(profile);
// when & then
// 잘못된 형식의 닉네임을 넣어줬을 때 제대로 BindingResult의 유효성 검사를 진행하는지 테스트합니다.
mockMvc.perform(patch("/members/profile")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token)
.content(profileJson))
.andExpect(status().isOk());
// 변경된 이후의 회원 프로필을 DB에서 뽑아옵니다.
MemberProfileResponseDto modifyProfile = memberProfileRepository.findMemberProfiles(2L, false).get(0);
// 비교하기
assertEquals(profile.getNickName(), modifyProfile.getNickName());
assertEquals(profile.getDescription(), modifyProfile.getDescription());
assertEquals(profileImage, modifyProfile.getProfileImage());
}
회원 프로필 수정 정상 동작 테스트는 mockMvc
를 사용하여 회원 프로필 수정을 요청한 이후 실제 DB에서 해당 프로필 정보를 가져온 후 수정 내역을 비교하는 식으로 진행하였습니다.
@DisplayName("로그아웃 테스트")
@Test
public void logoutTest() throws Exception {
// given
// Mock 객체 설정 - redisService 목 객체에 getRefreshToken() 메소드를 호출할 시 Null 값을 반환하도록 설정
Mockito.when(redisService.getRefreshToken(anyString())).thenReturn(null);
// when
mockMvc.perform(get("/removeToken")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", token))
.andExpect(status().isOk());
// then
// Mock 객체 호출 확인 - verify를 이용하여 redisService의 removeRefreshToken(refreshTokenKey)가 실제로 호출되었는지 확인합니다.
// 실제로는 IP 주소가 계속 바뀌므로 IP 주소는 동일하다는 가정하에 removeRefreshToken이 호출되었는지에 대한 테스트를 진행하였습니다.
Mockito.verify(redisService).removeRefreshToken(anyString());
// Mock 객체 설정에 의해 Null 값 반환 확인하는 작업입니다.
assertNull(redisService.getRefreshToken(refreshTokenKey));
}
로그아웃은 단순히 Redis에 토큰을 없애는 작업을 하는 로직입니다.
Mockito.verify
를 사용하여 Mock 객체의 메서드 호출 여부를 확인하는 식으로 테스트를 진행하였습니다.
@DisplayName("회원 탈퇴 테스트")
@Test
public void updateMemberDeleteTest() throws Exception {
// given
// Mock 객체 설정 - redisService 목 객체에 getRefreshToken() 메소드를 호출할 시 Null 값을 반환하도록 설정
Mockito.when(redisService.getRefreshToken(anyString())).thenReturn(null);
Member member = memberRepository.findById(2L).get();
// when
mockMvc.perform(delete("/members")
.header("Authorization", token))
.andExpect(status().isOk());
// then
// 회원, 프로필, 상세에 대해 softDelete 설정 했는지에 대한 테스트
Member findMember = memberRepository.findById(member.getId()).get();
assertEquals(true,findMember.isDeletedYn());
assertEquals(true,memberProfileRepository.findByMemberId(member.getId(),false).isEmpty());
assertEquals(true,memberDetailRepository.findByMemberId(member.getId(),false).isEmpty());
// Redis에 토큰 제거 테스트
Mockito.verify(redisService).removeRefreshToken(anyString());
assertNull(redisService.getRefreshToken(refreshTokenKey));
// 회원 프로필 이미지 삭제했는지 확인 테스트
assertEquals(true,fileService.findFilesByTableInfo(FileRequestDto.create(TableName.MEMBER_PROFILE, memberProfileRepository.findByMemberId(member.getId(), true).get().getId()), false).isEmpty());
// 회원이 등록한 게시글 및 게시글 관련 정보 삭제 테스트
}
회원 탈퇴 테스트는 회원 탈퇴 시 회원의 softDelete 여부 및 해당 회원과 관련된 파일, 게시글이 삭제되었는지를 검사합니다.
mockMvc
를 사용하여 회원 탈퇴 요청을 보낸 후 삭제 여부가 반영됐는지 실제 DB에 데이터를 가져와서 확인하는 식으로 테스트를 진행하였습니다.
Redis의 토큰 값 확인은 로그아웃 테스트와 같은 방식으로 진행하였습니다.