1. 개요
이전 포스팅에서 repository, service 계층의 단위 테스트를 작성해보았다. 이번에는 controller 계층의 단위테스트를 작성해보고자 한다.
2. Controller
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class) //JPA 관련 빈들을 mock으로 등록
class MemberControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
MemberService memberService;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("회원 활성화 완료 응답이 반환되어야 함")
@WithMockCustomUser
void activeMember() throws Exception {
//given
ActivateMemberDto activateMemberDto = new ActivateMemberDto("updatenick", "강원");
String content = objectMapper.writeValueAsString(activateMemberDto);
//when
ResultActions actions = mockMvc.perform(
patch("/api/v1/members/activate")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.with(csrf())
);
//then
actions
.andExpect(status().isOk())
.andExpect(jsonPath("$.customCode").value("MEMBER-SUCCESS-001"))
.andExpect(jsonPath("$.customMessage").value("회원 활성화 성공"))
.andExpect(jsonPath("$.status").value(true))
.andExpect(jsonPath("$.data").value(nullValue()));
...
}
위 코드는 회원의 추가 정보를 입력받는 기능의 컨트롤러 계층 테스트코드이다. 하나씩 살펴보자.
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class) //JPA 관련 빈들을 mock으로 등록
- @WebMvcTest(MemberController.class): 웹 계층에 관련된 컴포넌트들만을 로드한다.
- @MockBean(JpaMetamodelMappingContext.class): 본 프로젝트에서는 Auditing 기능을 사용한다. 이에 따라 본 애노테이션이 없으면 코드가 동작하지 않는다.
@Autowired
MockMvc mockMvc;
@MockBean
MemberService memberService;
@Autowired
ObjectMapper objectMapper;
MemberController는 MemberService를 의존받으므로 해당 클래스를 MockBean으로 등록해준다. 또, 요청을 보내기 위해 MockMvc를, DTO를 요청으로 만들기 위해 ObjectMapper를 실제 빈으로 등록해주었다.
@Test
@DisplayName("회원 활성화 완료 응답이 반환되어야 함")
@WithMockCustomUser
- @Test: 해당 메서드는 테스트코드임을 명시한다.
- @DisplayName: 해당 테스트의 이름을 명시한다.
- @WithMockCustomUser: 본 프로젝트는 스프링 시큐리티를 사용하고 있기 때문에 해당 요청에 대해서 가짜 사용자를 만들어 SecurityContext에 넣어주어야 한다. 자세한 내용은 후술
//given
ActivateMemberDto activateMemberDto = new ActivateMemberDto("updatenick", "강원");
String content = objectMapper.writeValueAsString(activateMemberDto);
given 파트이다. 임의의 dto를 하나 생성하고, objectmapper를 사용하여 json 형식으로 바꿔준다.
//when
ResultActions actions = mockMvc.perform(
patch("/api/v1/members/activate")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.with(csrf())
);
when 파트이다. 추가 정보를 입력받는 요청을 보낸다. 해당 경로에 patch method로 json 형식의 body(content)를 담아 mock csrf를 붙여 요청을 보낸다.(시큐리티 관련 설정은 추후 적용예정)
//then
actions
.andExpect(status().isOk())
.andExpect(jsonPath("$.customCode").value("MEMBER-SUCCESS-001"))
.andExpect(jsonPath("$.customMessage").value("회원 활성화 성공"))
.andExpect(jsonPath("$.status").value(true))
.andExpect(jsonPath("$.data").value(nullValue()));
then 파트이다. 해당 요청에 대한 응답은 200에, json의 각 요소에 대해 기대하는 값이 나오는 지 검증할 수 있다. 아래 링크에서 자세한 사용법을 알 수 있다.
https://github.com/json-path/JsonPath
GitHub - json-path/JsonPath: Java JsonPath implementation
Java JsonPath implementation. Contribute to json-path/JsonPath development by creating an account on GitHub.
github.com
3. @WithCustomMockUser
회원을 활성화하기 위해서는 앞서 언급했듯이 UserDetails를 SecurityContext에 사전에 담는 작업이 필요하다. 이를 가능케 하는 방법은 세 가지가 있다.
- @WithMockUser
- @WithUserDetails
- @WithSecurityContext
해당 프로젝트는 JWT를 사용함에, 위의 작업을 커스텀했으므로 3번째 방법을 사용하기로 했다. 우선 애노테이션을 하나 만들어준다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
long id() default 2L;
String nickname() default "테스트닉";
String username() default "username";
MemberRole role() default MemberRole.USER;
SignUpType signUpType() default SignUpType.KAKAO;
String profileImage() default "s3.com/testProfile";
String recommendLocation() default "서울";
MemberStatus status() default MemberStatus.ACTIVE;
;
}
해당 애노테이션에 사용자의 정보를 담고, @WithSecurityContext를 담는다. 그리고 securitycontext에 정보를 담을 클래스를 factory로 지정해준다.
public class WithMockCustomUserSecurityContextFactory implements
WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser mockCustomUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Member member = Member.builder()
.id(mockCustomUser.id())
.nickname(mockCustomUser.nickname())
.username(mockCustomUser.username())
.role(mockCustomUser.role())
.signUpType(mockCustomUser.signUpType())
.profileImage(mockCustomUser.profileImage())
.recommendLocation(mockCustomUser.recommendLocation())
.status(mockCustomUser.status())
.build();
UserPrincipal principal = new UserPrincipal(member);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
context.setAuthentication(authToken);
return context;
}
}
앞서 적은 회원 정보를 기반으로 UserPrincipal(extends UserDetails)를 만들어 SecurityContext에 넣어준다.
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
넣은 UserDetails는 위와 같이 다시 꺼내어 사용할 수 있다.
3. 여담
현재 설정이 덜 되어 각 요청에 .with(csrf())를 넣어주었다. 이후에는 이를 빼고, SecurityConfig를 적용해볼 것이다.
'Proj > Tripot' 카테고리의 다른 글
로그인 인증 과정 추가해보기 - AuthenticationProvider 커스텀 (1) | 2024.11.30 |
---|---|
application/json 형식으로 관리자 로그인 구현하기 (1) | 2024.11.29 |
스프링부트 테스트 코드 작성하기: Repository, Service (0) | 2024.11.18 |
스프링 시큐리티에서 발생하는 예외 처리하기 (0) | 2024.11.10 |
Trouble Shooting: @Value에 값이 불러와지지 않는 문제 (3) | 2024.11.04 |