티스토리 뷰

스프링 부트 REST API WEB 프로젝트

깃헙 링크

https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp

수행 목록

  1. 환경구성 및 helloworld 출력
  2. H2 DB 연동
  3. Swagger API 문서 연동
  4. REST API 설계
  5. RestControllerAdvice를 이용한 통합 예외 처리
  6. Entity - DTO 분리
  7. MessageSource를 이용해 예외 메시지 다국화
  8. JPA Aduting을 이용해 객체 생성시간/수정시간 적용
  9. 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
  10. 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
  11. Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
  12. JUnit Test (단위 테스트)
  13. JUnit Test (통합 테스트)
  14. OAuth 2.0 정리
  15. OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
  16. OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
  17. OAuth 2.0 카카오 로그인 테스트 검증
  18. 환경별 설정을 위해서profile 분리하기 

 

통합 테스트에 대해서 알아보자

 

@SpringBootTest

  • 실제 운영환경에서 사용될 클래스들을 통합하여 테스트한다.
  • 단위 테스트와 같이 기능검증을 위한 것이 아니라 스프링 프레임워크에서 전체적으로 로직이 제대로 동작하는지 검증하기 위해 사용한다.
  • 애플리케이션의 설정, 모든 Bean을 로딩하므로 운영환경과 가장 유사하게 테스트할 수 있다.
    • 단, 그만큼 느리다.

@SpringBootTest + @AutoConfigureMockMvc : SignController 테스트

@AutoConfigureMockMvc를 사용하면 MockMvc를 간편하게 사용할 수 있게해준다.

 

회원가입, 로그인에 대한 성공 / 실패 테스트를 만들어보자.
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class SignControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserJpaRepo userJpaRepo;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Before
    public void setUp() {
        userJpaRepo.save(User.builder()
                .name("woonsik")
                .password(passwordEncoder.encode("password"))
                .nickName("woonsik")
                .email("email@email.com")
                .roles(Collections.singletonList("ROLE_USER"))
                .build());
    }

    @Test
    public void 로그인_성공() throws Exception {
        String object = objectMapper.writeValueAsString(UserLoginRequestDto.builder()
                .email("email@email.com")
                .password("password")
                .build());
        //given
        ResultActions actions = mockMvc.perform(post("/v1/login")
                .content(object)
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON));

        //then
        actions.andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.msg").exists());
    }

    @Test
    public void 회원가입_성공() throws Exception {
        //given
        long time = LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond();

        String object = objectMapper.writeValueAsString(UserSignupRequestDto.builder()
                .email("email@email.com" + time)
                .nickName("woonsik")
                .name("woonsik")
                .password("myPassword")
                .build());
        ResultActions actions = mockMvc.perform(
                post("/v1/signup")
                        .content(object)
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON));

        //then
        actions.
                andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.msg").exists());
    }

    @Test
    public void 회원가입_실패() throws Exception {
        //given
        String object = objectMapper.writeValueAsString(UserSignupRequestDto.builder()
                .name("woonsik")
                .email("email@email.com")
                .password("password")
                .nickName("woonsik")
                .build());

        //when
        ResultActions actions = mockMvc.perform(post("/v1/signup")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(object));

        //then
        actions.andDo(print())
                .andExpect(status().is5xxServerError())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.code").value(-1002));
    }

    @Test
    public void 로그인_실패() throws Exception
    {
        //given
        String object = objectMapper.writeValueAsString(UserLoginRequestDto.builder()
                .email("email@email.com")
                .password("wrongPassword")
                .build());
        //when
        ResultActions actions = mockMvc.perform(post("/v1/login")
                .content(object)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON));
        //then
        actions
                .andDo(print())
                .andExpect(status().is5xxServerError())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.code").value(-1001));
    }
}

4가지 경우 모두 제대로 돌아가는 모습

설명

String object = objectMapper.writeValueAsString(요청 객체)

ObjectMapper를 이용해서 요청을 보낼 객체를 생성해준다.

mockMvc.perform(post("url") //get, post 등등
                .content(object) // 요청보내는 @RequestBody
                .contentType(MediaType.APPLICATION_JSON) // contentType을 json형식으로 설정
                .accept(MediaType.APPLICATION_JSON)); // header를 json형식으로 설정

MockMvc가 수행할 행동을 정의해준다.

perform

  • URL을 받아서 GET / POST / PUT / DELETE 등 다양한 메서드를 수행할 수 있다.
  • header에 값을 세팅할수 있다. content넣어주기
  • AcceptType을 설정해준다 (MediaType.APPLICATION_JSON)
actions.
	andDo(print()) // 내용 출력
	.andExpect(status().isOk()) // HTTP response Code 200 인지 확인
	.andExpect(jsonPath("$.success").value(true)); // json내 키값을 기준으로 비교, success가 true인지

해당 행동을 통해 일어난 결과에 대해 검증한다.

andDo()

  • perform요청을 처리한다.

andExpect()

  • 검증내용을 체크한다.
  • 결과가 200 OK인지 확인 == isOk() / 300 == isMovedPermanantly() / 3xx == is3xxRedirection() 등등 다양한 조건이 가능하다.
  • 반환된 Json객체에 대해서도 체크가 가능하다
    • .andExpect(jsonPath("$.success").value(false))

andReturn()

  • MvcResult 객체로 반환시켜준다.

SpringSecurity 테스트를 위한 @WithMockUser

유저에게 리소스의 사용권한이 있는지 없는지에 따른 테스트를 할 때 용이하다.

 

USER권한으로만 접근할 수 있는 리소스에 GUEST권한의 유저가 접근하는 경우
@Test
    @WithMockUser(username = "mockUser", roles = {"GUEST"})
    public void 접근실패() throws Exception {
        //given
        //when
        //then
        mockMvc.perform(get("/v1/users"))
                .andDo(print())
                .andExpect(status().is3xxRedirection())
                .andExpect(redirectedUrl("/exception/accessDenied"));;
    }
}

 

정상적인 권한으로 접근하는 경우
{
    @Test
    @WithMockUser(username = "mockUser", roles = {"GUEST", "USER"})
    public void 접근성공() throws Exception
    {
        //given
        //when
        //then
        mockMvc.perform(get("/v1/users"))
                .andDo(print())
                .andExpect(status().isOk());
    }
}


@SpringBootTest + @AutoConfigureMockMvc : UserController 테스트

User 관련 서비스를 처리하기 위해서는 인증된 유저로 요청해야 하므로 MockUser를 이용해서 접근해야한다.

  • 따라서 최상단에 @WithMockUser를 달아주었다. roles는 default로 USER이므로 명시하지 않았다.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@WithMockUser(username = "mockUser")
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    UserService userService;

    @Autowired
    UserJpaRepo userJpaRepo;

    @Autowired
    PasswordEncoder passwordEncoder;

    private static int id;

    @Before
    public void setUp() {
        User save = userJpaRepo.save(User.builder()
                .name("woonsik")
                .password(passwordEncoder.encode("password"))
                .nickName("woonsik")
                .email("email@email.com")
                .roles(Collections.singletonList("ROLE_USER"))
                .build());
        id = Math.toIntExact(save.getUserId());
    }

    @Test
    public void 회원조회_이메일() throws Exception {
        //then
        ResultActions actions = mockMvc.perform(
                get("/v1/user/email/{email}", "email@email.com")
                .param("lang", "en"));

        actions
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.email", is("email@email.com")))
                .andExpect(jsonPath("$.data.name", is("woonsik")))
                .andReturn();
    }

    @Test
    public void 회원조회_userId() throws Exception {
        //given
        ResultActions actions = mockMvc.perform(get("/v1/user/id/{id}", id)
                .param("lang", "en"));
        //when
        //then
        actions.andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.userId", is(id)))
                .andExpect(jsonPath("$.data.email", is("email@email.com")))
                .andExpect(jsonPath("$.data.name", is("woonsik")));
    }

    @Test
    public void 전체_회원조회() throws Exception {
        //then
        mockMvc.perform(get("/v1/users"))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    public void 회원수정() throws Exception {
        //given
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("userId", String.valueOf(id));
        params.add("nickName", "afterNickName");
        //when
        ResultActions actions = mockMvc.perform(put("/v1/user")
                .params(params));
        //then
        actions
                .andDo(print())
                .andExpect(jsonPath("$.success", is(true)))
                .andExpect(jsonPath("$.code", is(0)))
                .andExpect(jsonPath("$.data", is(id)));
    }

    @Test
    public void 회원삭제() throws Exception {
        //given
        //when
        ResultActions actions = mockMvc.perform(delete("/v1/user/{id}", id));
        //then
        actions
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success", is(true)))
                .andExpect(jsonPath("$.code", is(0)))
                .andExpect(jsonPath("$.msg").exists());
    }
}

mockMvc perform을 작성할 때

  • @PathVariable의 경우에는 (/url/{var}, var) 식으로 넣어주면된다. 여러개일경우 /{a}/{b}, a, b .. 로 반복해서 하면된다.
  • @RequestParam의 경우에
    • MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
      멀티 param이 필요한경우 위와같이 map객체를 생성후 필요한 값들을 매핑해준후 params( "만든 params" ) 로 넣어주면된다.
    • param("lang", "en")
      단일 param의 경우 바로 key-value 형태로 넣으면 된다.

jsonPath로 값을 접근할 때

  • json객체의 depth가 추가되면 {"$.변수.변수"} 이렇게 들어가주면 접근할 수 있다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday