코프링 프로젝트에서 Mockito.any() 사용 시 NPE 발생

alsdl0629·2024년 1월 22일
0

테스트 코드

목록 보기
2/2
post-thumbnail

이번 글에서는 코프링 프로젝트에서 Mockito.any() 사용시 NPE가 발생한 이유와 이를 해결하는 방법을 정리해 보려고 합니다.

상황

Mockito를 사용한 이유

프로젝트에서 Redis를 사용하고 있어서 테스트 코드를 작성하기 위해 Redis 테스트 환경을 구축해야 했습니다. 이렇게 되면 테스트 코드를 실행하기 위해 Redis를 설치하고, 세팅하는 작업을 해야되는 문제점이 있었습니다.

시간이 지나면서 이런 점이 번거롭고, 불필요하다고 생각했어서 직접 객체를 조작할 수 있게 도와주는 Mockito를 사용해서 Redis 설정을 하지 않더라도 테스트를 할 수 있도록 했습니다.

발생한 에러

@Test
fun `인증 코드 엔티티를 저장하고 인증 코드로 변환한다`() {
        // given
        val code = "111111"
        val phoneNumber = "010xxxxxxxx"

        val authCode = AuthCode(
            code = code,
            phoneNumber = phoneNumber,
        )

        val authCodeEntity = AuthCodeEntity(
            code = code,
            phoneNumber = phoneNumber,
        )

        given(authCodeMapper.toDomain(any(AuthCodeEntity::class.java)))
            .willReturn(authCode)

        given(authCodeMapper.toEntity(any(AuthCode::class.java)))
            .willReturn(authCodeEntity)

        given(authCodeEntityRepository.save(any(AuthCodeEntity::class.java)))
            .willReturn(authCodeEntity)

        // when
        val savedAuthCode = authCodeProcessor.saveAuthCode(authCode)

        // then
        assertThat(savedAuthCode).usingRecursiveComparison().isEqualTo(authCode)
    }

BDDMockito.any를 사용해서 지정한 타입의 어떤 객체가 들어와도 같은 값을 반환하도록 했습니다.

BDDMockitoBDD 스타일로 작성할 수 있도록 도와주는 라이브러리이기 때문에 BDDMockito.any 내부에는 로직이 없고, Mockito의 기능을 BDD 스타일로 사용할 수 있도록 래핑해주는 역할을 합니다.

BDDMockito.any()ArgumentMatchers.any()를 호출합니다.

java.lang.NullPointerException: any(...) must not be null

테스트를 실행해보니 에러가 발생했습니다.

문제 원인

ArgumentMatchers.any() 내부에서 defaultValue()를 호출하고 있습니다.
defaultValue()는 객체의 기본값을 반환합니다.

defaultValue()를 호출하면 reference typedefault value인 null을 반환하는 것을 확인할 수 있습니다.


문제의 테스트 코드(코틀린)를 자바로 변환

ArgumentMatchers.any() 결과 값을 var9 변수에 저장하고 var9가 null인지 확인합니다.

Intrinsics.checkNotNullExpressionValue(var9, "any(AuthCodeEntity::class.java)");

이 코드가 추가된 이유는 코틀린의 Platform Type 때문입니다.

Platform Type이란?
자바 등의 다른 프로그래밍 언어와 상호작용할 때 생성되는 특별한 타입입니다.
다른 프로그래밍 언어에서 전달되어서 nullable인지 아닌지 알 수 없습니다.

Object var9 = ArgumentMatchers.any(AuthCodeEntity.class);

테스트 코드(코틀린)에서 ArgumentMatchers.any()와 같이 자바 코드와 상호작용하면서 자바에서 생성된 객체를 넘겨받은 var9 변수는 Platform Type입니다.

테스트 코드에서는 null을 허용하지 않았는데 Platform Type은 nullable일 수도 있기 때문에 null인지 아닌지를 검사하기 위해 Intrinsics.checkNotNullExpressionValue() 를 사용합니다.

하지만 위에서 호출한 결과처럼 ArgumentMatchers.any()에서는 null을 반환하기 때문에 Intrinsics.checkNotNullExpressionValue() 에서 NPE가 발생했습니다.

정리해보면 자바와 코틀린에서 null을 다루는 방법이 다르기 때문에 발생한 문제였습니다.


해결 방법

mockito는 이런 문제를 해결하기 위해 코틀린을 위한 mockito-kotlin을 제공합니다.

testImplementation "org.mockito.kotlin:mockito-kotlin:x.x.x"

의존성을 추가한 뒤 org.mockito.kotlin.any를 사용하니 테스트가 성공했습니다.

inline fun <reified T : Any> any(): T {
    return ArgumentMatchers.any(T::class.java) ?: createInstance()
}

org.mockito.kotlin.any 코드를 확인해보니
코틀린으로 ArgumentMatchers.any()를 감싸서 non-null하게 만듭니다.


느낀점

자바와 코틀린에서 null을 다루는 방법이 다르다는 것을 자세하게 알게 되었고,
이를 해결하기 위해 코틀린에서는 Platform Type을 제공한다는 것을 알게 되었습니다.

나중에는 kotlin 진영의 단위 테스트 라이브러리인 mockk를 사용해서 단위 테스트를 작성해보려고 합니다.

그리고 mock을 사용하게 된 이유가 Redis 테스트 구축 환경을 구성하기 어려워셔였는데 찾아보니 인메모리 데이터베이스인 H2처럼 Embedded Redis라는 게 있다고 해서 이걸 사용해서 통합 테스트를 해도 괜찮을 것 같다는 생각을 하게 되었습니다.

개인적으로 이메일 전송처럼 외부 시스템을 사용하는게 아니면 최대한 넓은 범위의 실제 객체를 가지고 테스트 할 수 있는 통합 테스트를 하는게 좋은 것 같습니다.


참고 자료 🙇🙇🙇

https://github.com/emiling/TIL/issues/8
https://thdev.tech/kotlin/2020/11/24/kotlin_effective_12/

profile
인풋보다 아웃풋

0개의 댓글