코딩하는 털보

21.11.09 TIL 본문

Diary/Today I Learned

21.11.09 TIL

이정인 2021. 11. 10. 02:00

오늘의 삽질

테스트 코드에서 자바 리플렉션으로 setter 사용하기

TimeStamped 클래스는 엔티티가 상속받는 Entity Listener 클래스이다.
이 클래스를 상속받은 엔티티가 저장될 때의 시점을 createdAt 필드에 저장한다.
보는바와 같이 setter가 없기 때문에 외부에서 수정할 수 없다!

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeStamped {

    @CreatedDate
    private LocalDate createdAt;

}
public class User extends TimeStamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

TimeStamped는 비즈니스 로직에서 조회는 많이 할 수 있지만 딱히 수정할 만한 이유가 없을 것이다.
문제는 테스트 코드에서 발생한다.

단위 테스트인 경우 데이터베이스를 사용하지 않는다. 일반적으로 데이터 베이스에서 받아올 객체를 Mockito.given() 메서드를 통해
가짜 객체를 반환하도록 미리 약속한다.
아래는 로그인 테스트 코드이며 미리 mock 생성한 testUser라는 User 객체를 사용한다.

@Test
    void login() throws Exception {
        //given
        String code = "abcdefg1234567";
        String socialName = "google";
        String mockAccessToken = "accessToken";
        String mockRefreshToken = "refreshToken";

        given(socialLoginService.getUserInfo(socialName, code))
                .willReturn(mockUserInfo);
        given(oauth2UserService.putUserInto(mockUserInfo))
                .willReturn(mockUserInfo);
        given(jwtTokenProvider.responseAccessToken(testUser))
                .willReturn(mockAccessToken);
        given(jwtTokenProvider.responseRefreshToken(testUser))
                .willReturn(mockRefreshToken);

        //when
        mvc.perform(get("/user/login/google")
                        .param("code", code))
                .andDo(print())

                //then
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.isFirstLogin").value(false))
                .andExpect(jsonPath("$.refreshToken").value(mockRefreshToken))
                .andExpect(jsonPath("$.accessToken").value(mockAccessToken))
                .andExpect(jsonPath("$.statusCode").value(200))
                .andExpect(jsonPath("$.responseMessage").value("토큰 발급 완료"));

        verify(socialLoginService).getUserInfo(socialName, code);
        verify(oauth2UserService).putUserInto(mockUserInfo);
        verify(jwtTokenProvider).responseAccessToken(testUser);
        verify(jwtTokenProvider).responseRefreshToken(testUser);
    }

문제는 testUser 객체가 실제로 DB에 저장된 것이 아니고 그냥 만들어진 객체라는 것이다.
(User.create() 메서드는 User 클래스의 정적 팩토리 메서드이다.)

private User testUser;
    private Oauth2UserInfo mockUserInfo;

    @BeforeEach
    void before() {
        Map<String, Object> attributes = new HashMap<>();
        attributes.put("sub", "123456789");
        attributes.put("name", "tester");
        attributes.put("email", "tester.test.com");
        mockUserInfo = new GoogleOauth2UserInfo(attributes);
        testUser = User.create(mockUserInfo);
    }

testUser는 DB에 저장된 것이 아니므로 createdAt은 당연히 null이다.
만약 비즈니스 로직에서 createdAt을 사용하는 부분이 있다면 NullPointerException이 발생하기 좋은 조건인 것이다.
그렇다고 테스트에서 사용하기 위해서 TimeStamped에 setter를 달아줘야 하는지도 고민했지만
아무리 생각해도 배보다 배꼽이 큰 느낌이다.
방법을 이래저래 찾아본 결과...

PowerMock

PowerMock은 테스트 코드에서 쉽게 리플렉션을 사용할 수 있게 도와주는 라이브러리이다.

gradle 의존성

    testImplementation 'org.powermock:powermock-module-junit4:2.0.4'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.4'

참고로 우리는 junit5를 사용하며 사용한 powermock은 junit4 호환이다.
아직 junit5용 powermock은 없는 듯 하다. 하지만 사용에 문제는 없었다.

@WebMvcTest(controllers = Oauth2Controller.class)
@AutoConfigureMockMvc(addFilters = false)
@RunWith(PowerMockRunner.class)
class Oauth2ControllerTest {

PowerMockRunner
테스트에 @RunWith(PowerMockRunner.class) 어노테이션으로 이 테스트에서 powermock을 사용할 수 있도록 해준다.

@WebMvcTest(controllers = Oauth2Controller.class)
@AutoConfigureMockMvc(addFilters = false)
@RunWith(PowerMockRunner.class)
class Oauth2ControllerTest {

org.powermock.reflect.WhiteBox.setInternalState()

WhiteBox.setInternalState() 메서드는 리플렉션으로 특정 객체의 특정 필드에 직접 접근할 수 있게 해준다.
아래 코드는 testUser 객체의 createdAt 필드를 현재시간으로 변경시킨다.

    @BeforeEach
    void before() {
        testUser = User.create(mockUserInfo);
        Whitebox.setInternalState(testUser, "createdAt", LocalDate.now());
    }

테스트 코드를 작성할 때마다 setter나 기본 생성자가 없으면 Mock 객체를 생성하는 과정이 매우 불편하기 때문에 추가하고 싶은 마음이 굴뚝같다.

powermock은 위의 예시 외에도 여러 기능들로 이런 어려움을 해결할 수 있도록 도와준다.

Comments