Spring Boot 에서 MemberController 테스트 코드 작성하기

Ada·2022년 12월 10일
0

항해TOL

목록 보기
54/63

우리 팀이 작성한 코드가 제대로 잘 동작하는지 시험 해보고, 테스트 코드 작성을 어떻게 하는지 공부해보기 위해서 스프링 부트에 내장되어있는 JUnit을 이용해서 MemberController 클래스의 테스트 코드를 작성해보았다.

버전은 JUnit5를 사용했으며 크게 맞닥뜨린 문제들만 기술 해보고 컨트롤러단의 전체 테스트 코드는 맨 아래에 첨부하겠다.


첫 시도 - Entity 기준 테스트 코드

처음엔 @SpirngBootTest 를 사용하여 Entity 기준으로 테스트를 진행해봤었는데, 테스트 코드 실행 속도가 굉장히 느리다고 느껴졌다.

또한 HttpServletReqeust, HttpServletResponse 등의 객체는 테스트 코드 작성 시 파라미터로 넘기기가 어려워서 어떻게 해야 할지 고민하던 차에 항해에서 알게 된 기술매니저님이자 좋은 멘토님이신 인혁 매니저님께서 MockMvc에 대해 알려주셨다.

사실 구글링 하면서 보긴 봤었는데, 그 때는 Mock을 정확히 왜 사용해야 하는지 잘 몰라서 그냥 넘겨버렸던 것 같다.

MockMvc

MockMvc : 요청 및 전송, 응답기능을 제공해주는 유틸리티 클래스

MockMvc를 사용하면 가짜 객체를 만들어서 애플리케이션 서버에 배포하지 않고도 스프링 MVC 동작을 재현할 수 있기 때문에 테스트 코드 작성을 굉장히 용이하게 해준다.

그리고 테스트 코드에 대해서 공부할 때 @SpringBootTest 어노테이션은 꼭 필요한 Bean 말고도 소스 코드 내에 생성된 모든 Bean을 불러온다고 보아서 이번엔 아예 필요한 Bean만을 호출하여 사용하는 @WebMvcTest 어노테이션을 이용해서 테스트 코드 작성을 시도해보았다.


아쉽지만 코드를 계속해서 수정하느라 남은 건 캡쳐본 뿐이고, 수정 전의 소스코드들은 존재하지 않아서
캡쳐로나마 자료를 남겨본다.

맨 처음 @WebMvcTest를 이용하여 테스트 코드를 작성하려고 설정했던 코드이다.

MockMvc만 @Autowired로 의존 관계 주입을 해주었고,
Controller를 제외하고 필요한 필드들을 @MockBean 어노테이션을 통해 빈으로 등록해준다.

처음에 회원가입 성공을 위해 작성했던 테스트 코드이다.

회원가입 시 RequestBody 로 dto를 받고 있기 때문에

Dto를 만들어서 json 형태의 String 으로 변환하여 content에 넣어 넘겨주었다.

++
(사실 저 dto를 json-> string -> content 로 변환하는 과정에서 굉장히 애를 먹었다....
RequestBody 형태는 무조건 MultiValueMap으로 만들어야 한다고 들었어서 계속 map 형태로 데이터를 만들고, 다시 objectMapper를 이용해서 형 변환을 하는 방식을 시도하다가 매니저님의 조언을 듣고 이것저것 다 빼버렸더니 오히려 작동이 잘 되었다. RequestBody 형식이라고 무조건 MultiValueMap 형태를 사용해야 하는건 아니였다!)

403 forbidden

그랬더니 예상치 못한 403 forbidden 에러가 났다.

열심히 구글링 해본 결과, csrf 설정을 하지 않아서 발생한 오류라고 하기에 아래처럼
.with(csrf) 를 추가하였더니 403 에러는 더 이상 발생하지 않았다.

401 Unauthorized


csfr() 를 추가해준 뒤 다시 테스트를 실행시켜보자 이번엔 401 에러가 발생했다.

이 역시 구글링과 매니저님의 조언으로 답을 찾을 수 있었는데,
@WebMvcTest는 스프링 시큐리티에서 우리가 설정해 둔 security configuration 이 작동하지 않아서
회원가입 페이지에 대한 비 회원의 권한이 사라져버린 것이였다.

회원가입 페이지인데 로그인이 필요한 상황이라니 이건 정말 말도 안 된다고 생각해서 이것저것 찾아본 결과

@WebMvc 테스트에서는 @WithUserDetails 어노테이션을 사용하여 main 코드의 spring security에 대한 설정들을 상속받을 수 있다는 것을 알게되었다.

그래서 @WithUserDetails 어노테이션을 최 상단 클래스에 달아주어서 회원가입 테스트를 진행해보니 잘 통과 되었다.

그런데 다음 문제는, 로그인 실패시의 expect code가 actual code와 다르다는 것이였다.

@WithUserDetails 어노테이션이 무조건 올바른 회원 정보를 할당해주고 있기 때문에
실패 케이스 테스트 코드에서 계속해서 assertion error가 발생할 수 밖에 없었다...

이를 해결하기 위해 @WithMockCustomUser 를 선언하여 새로운 Bean을 만드는 방법을 시도해보았는데,
이번엔 아예 NPE 에러가 발생하여서 다시 다른 방법을 강구해봐야했다.

구글링을 통해 정보를 열심히 찾아보고, 깃허브의 소스 코드들도 많이 보았지만
로그인, 회원가입 부분의 테스트 코드는 대부분 SpringBootTest로 진행한 것을 볼 수 있었다.

그래서 결국 결론은....

@WebMvcTest 를 포기하고 @SpringBootTest로 진행하여서 controller 테스트 코드 통과에 성공하였다....

처음부터 SpringBootTest로 진행했으면 많은 시간을 아낄 수 있었을 것 같아서 아쉽지만
그래도 테스트 코드에 대해 많이 공부할 수 있던 기회가 된 것 같아서 나름 보람차다.

아래는 테스트 코드 전문이다.

package com.move.TripBalance.member.controller;

import com.google.gson.Gson;
import com.move.TripBalance.member.Member;
import com.move.TripBalance.member.controller.request.IdCkeckRequestDto;
import com.move.TripBalance.member.controller.request.LoginRequestDto;
import com.move.TripBalance.member.controller.request.MemberRequestDto;
import com.move.TripBalance.member.controller.request.NickNameCheckRequestDto;
import com.move.TripBalance.member.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;

import javax.transaction.Transactional;
import java.util.Optional;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@Nested
@DisplayName("Member Controller 테스트")
class MemberControllerTest {

    @Autowired
    MockMvc mvc;

    @Autowired
    MemberRepository memberRepository;

    @Nested
    @DisplayName("회원가입 테스트(이메일, 닉네임, 패스워드)")
    class SignupTest {
        @Test
        @DisplayName("회원가입 성공")
        void signup() throws Exception {

            // 회원 가입을 위한 dto
            MemberRequestDto dto = MemberRequestDto.builder()
                    .email("signuptest@test.com")
                    .nickName("signup")
                    .pw("test1234!")
                    .pwConfirm("test1234!")
                    .build();

            String json = new Gson().toJson(dto); // dto 를 json 형식의 String 으로 만들기

            // api 전송
            mvc.perform(post("/tb/signup")// 요청 전송
                            .with(csrf()) // 403 에러를 방지하기 위한 csrf
                            .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                            .content(json))
                    .andExpect(status().isOk()) // 성공 코드 반환
                    .andDo(print()); // 요청과 응답 정보 전체 출력

            Optional<Member> member = memberRepository.findByNickName("signup");

            memberRepository.delete(member.get());
        }
    }

        @Nested
        @DisplayName("로그인 테스트(이메일, 패스워드)")
        class LoginTest {
            @Test
            @Transactional
            @DisplayName("로그인 성공")
            void login() throws Exception {

                // 로그인을 위한 dto
                LoginRequestDto loginDto = LoginRequestDto.builder()
                        .email("user1@test.com")
                        .pw("pass1234^^")
                        .build();

                // dto 를 json 형식의 String 으로 만들기
                String json = new Gson().toJson(loginDto);

                // api 전송
                mvc.perform(post("/tb/login")// 요청 전송
                                .with(csrf()) // 403 에러를 방지하기 위한 csrf
                                .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                                .content(json))
                        .andExpect(status().isOk()) // 성공 코드 반환
                        .andDo(print()); // 요청과 응답 정보 전체 출력
            }

            @Nested
            @DisplayName("로그인 실패")
            class LoginFailed {

                @Test
                @DisplayName("이메일 유효성 없음")
                void notEmail() throws Exception {

                    // 로그인을 위한 dto
                    LoginRequestDto dto = LoginRequestDto.builder()
                            .email("failtest2@test.com")
                            .pw("pass1234^^")
                            .build();

                    // dto 를 json 형식의 String 으로 만들기
                    String json = new Gson().toJson(dto);

                    // api 전송
                    mvc.perform(post("/tb/login")// 요청 전송
                                    .with(csrf()) // 403 에러를 방지하기 위한 csrf
                                    .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                                    .content(json))
                            .andExpect(status().isNonAuthoritativeInformation()) // 에러 코드 반환
                            .andDo(print()); // 요청과 응답 정보 전체 출력
                }

                @Test
                @DisplayName("비밀번호 유효성 없음")
                void notPw() throws Exception {

                    // 이미 존재하는 이메일
                    String existEmail = "user1@test.com";

                    // 로그인을 위한 dto
                    LoginRequestDto dto = LoginRequestDto.builder()
                            .email(existEmail)
                            .pw("failpw1234!")
                            .build();

                    // dto 를 json 형식의 String 으로 만들기
                    String json = new Gson().toJson(dto);

                    // api 전송
                    mvc.perform(post("/tb/login")// 요청 전송
                                    .with(csrf()) // 403 에러를 방지하기 위한 csrf
                                    .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                                    .content(json))
                            .andExpect(status().isNonAuthoritativeInformation()) // 에러 코드 반환
                            .andDo(print()); // 요청과 응답 정보 전체 출력
                }
            }
        }

            @Test
            @WithUserDetails(value = "user1@test.com")
            @DisplayName("로그아웃 성공")
            void logout() throws Exception {

                // api 전송
                mvc.perform(post("/tb/logout")// 요청 전송
                                .with(csrf())) // 403 에러를 방지하기 위한 csrf
                        .andExpect(status().isOk()) // 성공 코드 반환
                        .andDo(print()); // 요청과 응답 정보 전체 출력
            }

            @Nested
            @DisplayName("중복 체크")
            class existCheck {
                @Test
                @DisplayName("이메일 중복 체크")
                void idcheck() throws Exception {

                    // 이미 존재하는 이메일
                    String existEmail = "user1@test.com";

                    // 로그인을 위한 dto
                    IdCkeckRequestDto dto = IdCkeckRequestDto.builder()
                            .email(existEmail)
                            .build();

                    // dto 를 json 형식의 String 으로 만들기
                    String json = new Gson().toJson(dto);

                    // api 전송
                    mvc.perform(post("/tb/signup/idcheck")// 요청 전송
                                    .with(csrf())
                                    .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                                    .content(json))// 403 에러를 방지하기 위한 csrf
                            .andExpect(status().isImUsed()) // 에러 코드 반환
                            .andDo(print()); // 요청과 응답 정보 전체 출력
                }

                @Test
                @DisplayName("닉네임 중복 체크")
                void nicknamecheck() throws Exception {

                    // 이미 존재하는 닉네임
                    String existNickName = "user1";

                    // 로그인을 위한 dto
                    NickNameCheckRequestDto dto = NickNameCheckRequestDto.builder()
                            .nickName(existNickName)
                            .build();

                    // dto 를 json 형식의 String 으로 만들기
                    String json = new Gson().toJson(dto);

                    // api 전송
                    mvc.perform(post("/tb/signup/nicknamecheck")// 요청 전송
                                    .with(csrf())
                                    .contentType(MediaType.APPLICATION_JSON)// json 형식으로 데이터를 보낸다고 명시
                                    .content(json))// 403 에러를 방지하기 위한 csrf
                            .andExpect(status().isImUsed()) // 에러 코드 반환
                            .andDo(print()); // 요청과 응답 정보 전체 출력
                }
            }

}
profile
백엔드 프로그래머

0개의 댓글