Spring Boot Test 적용기

Kyu0·2022년 12월 1일
0

Spring Boot

목록 보기
2/4

이번 게시글에서는 Spring Boot Test를 적용하면서 마주친 여러 이슈들을 소개하고 어떻게 해결했는지에 대해 작성하겠습니다.

관련 깃허브 레포지토리


@SpringBootTest vs @WebMvcTest

@SpringBootTest 어노테이션은 Spring Boot 프로젝트에서 사용하는 모든 Bean 객체를 로드하고 테스트를 수행합니다.
그에 반해, @WebMvcTest 어노테이션은 Web 환경과 관련된 Controller, Service과 같은 객체들만 로드하고 테스트를 수행합니다.

제가 적용한 코드는 다음과 같습니다.

@WebMvcTest(PostCategoryApiController.class)
public class PostCategoryControllerTest {
    //...
}

만약 JPA와 관련된 테스트 코드를 작성한다면 @DataJpaTest, REST API와 관련된 테스트 코드를 작성한다면 @RestClientTest 등의 어노테이션을 사용할 수 있습니다.

프로젝트 규모가 커짐에 따라 로드해야하는 Bean 객체의 개수도 많아지므로 현재 작성하는 테스트 코드의 목적에 따라 알맞은 테스트 어노테이션을 사용한다면 테스트 수행 시간도 짧아질 수 있을 것이라 생각합니다..!


@WithMockUser

제가 진행하고 있는 프로젝트의 REST API들은 모두 로그인한 사용자만이 접근할 수 있도록 설정되어 있습니다. 따라서 REST API를 테스트하려면

  1. 테스트할 때만 접근 권한 설정 해제
  2. MockMvc 객체의 헤더에 JWT 토큰을 추가하여 요청하기

1번의 해결 방법을 사용하자니 앞으로도 많은 테스트 코드를 작성하고 수행할텐데 그때마다 Spring Security 설정을 주석 처리/해제 해야하는 과정이 번거로울 것이라 생각했고
2번의 해결 방법을 사용하자니 테스트 코드에 민감한 정보를 포함하는 것 같아 다른 방법을 찾아보기로 했습니다.

다행히도 @WithMockUser 라는 어노테이션이 있다는 것을 알게되었고 해당 어노테이션을 사용하니 403 에러 응답 대신 올바른 응답이 오는 것을 확인했습니다.

사용법은 다음과 같습니다.

@Test
@WithMockUser(username="id", password="password", roles={"USER", "ADMIN"})
public void 테스트() {
	//...
}

refEq()

final String TEST_NAME = "테스트이름";
final SaveRequest REQUEST_DTO = new SaveRequest(TEST_NAME);

when(postCategoryService.save(REQUEST_DTO))
            .thenReturn(new PostCategory(1, TEST_NAME, new ArrayList<>()));
            
mockMvc.perform(post(URL)
        .content(mapper.writeValueAsString(REQUEST_DTO))
        .with(csrf())
        .contentType(MediaType.APPLICATION_JSON));
            
verify(postCategoryService).save(REQUEST_DTO);

위와 같이 Mockito를 사용해 작성한 테스트 코드는 잘 수행되지 않았습니다. 아마 when(postCategoryService.save(REQUEST_DTO))에서 설정한 REQUEST_DTO와 MockMvc에서 전달한 REQUEST_DTO가 다르다고 판단했을 것이라 생각하여 해결 방안을 찾아봤습니다.

그 결과, refEq(Object) 메소드를 이용하라는 내용들을 찾았습니다. refEq(Object)에 대한 설명은 다음과 같습니다.

Object argument that is reflection-equal to the given value with support for excluding selected fields from a class.

This matcher can be used when equals() is not implemented on compared objects. Matcher uses java reflection API to compare fields of wanted and actual object.

Works similarly to EqualsBuilder.reflectionEquals(this, other, excludeFields) from apache commons library.

Warning, The equality check is shallow!

요약하자면 equals() 메소드가 오버라이드 되지 않은 객체에 대해 동등성 비교를 하기 위해 사용하는 메소드라는 설명입니다. (참고자료, [Java] 동일성(identity)과 동등성(equality), 제이온님 티스토리)

우리가 작성한 자바 클래스들은 암시적으로 Object 클래스를 상속받고 있습니다. 그리고 Object 클래스의 equals() 메소드는 다음과 같습니다.

public boolean equals(Object obj) {
	return (this == obj);
}

그리고 자바에서 객체끼리의 ==(Equality Operator) 비교는 객체가 가진 주소를 비교합니다. 이 사실을 생각하며 제가 작성한 코드를 다시 보겠습니다.

when(postCategoryService.save(REQUEST_DTO))
    .thenReturn(new PostCategory(1, TEST_NAME, new ArrayList<>());

여기서 PostCategoryService를 기반으로 만들어진 MockBean의 save 메소드가 REQUEST_DTO를 인자로 받으면 PostCategory를 생성해 리턴해주는 것으로 명시를 했습니다.

하지만 SaveRequest 클래스의 equals() 메소드가 오버라이드 되지 않았기 때문에 MockBean은 인자로 받은 SaveRequest 객체의 동일성을 비교하게 되어 객체의 값을 비교하는 것이 아니라 주소값을 비교하도록 동작이 됩니다.

제가 추측한 문제가 발생한 이유는 다음과 같습니다.(아무리 검색해도 찾을 수가 없네요ㅠ 틀렸다면 지적바랍니다!)

mockMvc.perform(post(URL          
    .content(mapper.writeValueAsString(REQUEST_DTO))
    .with(csrf())
    .contentType(MediaType.APPLICATION_JSON))

REQUEST_DTO를 JSON 형태의 문자열로 직렬화한 뒤 컨트롤러에 HTTP 요청을 보내면 컨트롤러는 해당 HTTP 요청의 Body을 다시 역직렬화해 새로운 SaveRequest를 만들게 됩니다.

이 과정에서 만들어진 SaveRequest 객체는 기존에 우리가 선언한 REQUEST_DTO와 다른 주소값을 가지기 때문에 MockBean이 이를 제대로 인식하지 못하고 리턴 값을 주지 못하는 것입니다.

따라서 refEq() 메소드를 이용해 인자로 넘겨받은 객체들을 리플렉션하고 리플렉션된 객체들의 필드들을 비교하여 동등성이 같은지를 비교해야 합니다.

적용한 코드는 다음과 같습니다.

final String TEST_NAME = "테스트이름";
final SaveRequest REQUEST_DTO = new SaveRequest(TEST_NAME);

when(postCategoryService.save(refEq(REQUEST_DTO)))
            .thenReturn(new PostCategory(1, TEST_NAME, new ArrayList<>()));
            
mockMvc.perform(post(URL)
        .content(mapper.writeValueAsString(REQUEST_DTO))
        .with(csrf())
        .contentType(MediaType.APPLICATION_JSON));
            
verify(postCategoryService).save(refEq(REQUEST_DTO));

질문, 잘못된 내용, 오타 지적 언제나 환영입니다.

profile
개발자

0개의 댓글