이번 글에서는 코프링 프로젝트에서
Mockito.any()
사용시 NPE가 발생한 이유와 이를 해결하는 방법을 정리해 보려고 합니다.
프로젝트에서 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
를 사용해서 지정한 타입의 어떤 객체가 들어와도 같은 값을 반환하도록 했습니다.
BDDMockito
는 BDD 스타일로 작성할 수 있도록 도와주는 라이브러리이기 때문에BDDMockito.any
내부에는 로직이 없고,Mockito
의 기능을 BDD 스타일로 사용할 수 있도록 래핑해주는 역할을 합니다.
BDDMockito.any()
는ArgumentMatchers.any()
를 호출합니다.
java.lang.NullPointerException: any(...) must not be null
테스트를 실행해보니 에러가 발생했습니다.
ArgumentMatchers.any()
내부에서 defaultValue()
를 호출하고 있습니다.
defaultValue()
는 객체의 기본값을 반환합니다.
defaultValue()
를 호출하면 reference type
의 default 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/