[Test] 회원 관련 로직에 대한 테스트 코드 작성

김강욱·2024년 4월 25일
1

Project-Evertrip

목록 보기
5/19
post-thumbnail

😃 통합 테스트 진행

해당 프로젝트에서는 테스트 단위를 넓게 잡는 방식으로 통합 테스트를 진행하였습니다.

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에서 가져온 객체의 속성 값과 비교하였습니다.


회원 프로필 수정 BindingResult 검증 테스트

    @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를 넣어 요청을 생성해주었습니다.

각각의 조건에 맞는 예외를 내뱉는지 확인을 해주었습니다.


회원 프로필 수정 Invalid fileId 테스트

    @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의 토큰 값 확인은 로그아웃 테스트와 같은 방식으로 진행하였습니다.

profile
TO BE DEVELOPER

0개의 댓글

관련 채용 정보