입사하면서 코틀린을 시작하여, 사용한지 2달밖에 되지 않아 어려움이 있지만, 그만큼 재미있게 배우고 있습니다 :)
사내에서 Java & Mybatis 에서 Kotlin & Jpa 전환을 진행하면서, 코틀린 테스트코드에 대한 기틀을 잡아가고 있습니다. 이 과정에서 코틀린 테스트 환경 구축을 주도적으로 하며, kotest + mockK + fixture 적용 가이드를 작성하였고, 배우고 적용했던 내용을 부서분들에게 공유하는 시간을 가졌습니다.
그 내용들을 나누어 올려볼까 합니다.
: 독립적인 객체 모킹. Spring Context 외부에서 사용.
사용 예시 : private val userRepository: UserRepository = mockk(relaxed = true)
relaxed = true 옵션
mockk(relaxed = true) : primitive 값들은 기본값을 반환하도록 함 (0, false, “”)
만약 relaxed = true 옵션을 주지 않고 stub하지 않은 메서드를 호출하면 “목킹을 했으나 해당 목킹에 대한 반환값이 정해지지 않았다”는 에러 반환
언제 사용해야 하는가?
- relaxed : 간단한 테스트나 빠른 테스트 개발이 필요할 때 → 기본값을 반환함으로서 유연하고 빠른 테스트 작업을 반환할 수 있음
- default: 엄격한 테스트나 특정 동작을 명확하게 검증해야 하는 경우 → 실수로 잘못된 메서드를 호출했을 때 이를 인지하기 어려울 수 있음
JUnit, Kotest 등의 테스트 프레임워크( Spring Context 외부) 에서 특정 객체를 모킹할 때 사용.
@MockK lateinit var displayCommentService: DisplayCommentService
mockk()처럼 동작하지만, 필드를 위한 모킹을 자동화. 클래스 필드에 mock 객체를 자동 주입
객체 초기화 필요: @MockK는 기본적으로 초기화되지 않으므로, 테스트 클래스에 @ExtendWith(MockKExtension::class)나 MockKAnnotations.init(this)를 호출하여 초기화해야
@RelaxedMockK : relaxed = true 설정된 MockK임 (@MockK(relaxed=true) 와 동일)
@MockK와 @RelaxedMockK과의 차이에 대해 더 자세히 알고 싶으면 해당 글 참고
@MockK 예시 코드
abstract class UnitTest {
@Rule
@JvmField
val injectMocks = TestRule { statement, _ ->
MockKAnnotations.init(this, relaxUnitFun = true)
statement
}
}
class SampleViewModelTest : UnitTest() {
private lateinit var viewModel: SampleViewModel
@MockK
private lateinit var repository: SampleRepository
@Before
fun before() {
viewModel = SampleViewModel(repository)
}
@Test
fun test01_insert() {
val sample = mockk<Sample>()
viewModel.insert(sample)
verify { repository.insert(sample) }
}
}
코드 출처 : https://leveloper.tistory.com/199
Spring Context 내에서 의존성을 모킹하여 주입. Spring 테스트에 적합.
해당 stack overflow 글을 읽어보면 mockK()와 @MockK중 어느 것을 사용하는 것이 좋은지에 대한 토론이 있다.
요약해보면 아래와 같다
mockK() 사용하자
@MockK 사용하자
모든 테스트에 @SpringBootTest에 @MockKBean 무지성 사용.. 참 편하다. 물론 우리는 통합테스트는 필요할 때만, 테스트 범위는 최소한으로, 필요한 것만 Spring 에서 가져와서 사용해야 하는 것을 알고있다.
그렇다면 @MockKBean은 편하게 막 사용해도 되는가? 이에 대한 답변을 아래 블로그에서 발췌해보았다.
The Spring test framework will cache an ApplicationContext whenever possible between test runs.
In order to be cached, the context must have an exactly equivalent configuration.
Whenever you use @MockBean, you are by definition changing the context configuration.스프링 테스트 프레임워크는 테스트가 돌아가면서 ApplicationContext를 캐싱할 것이다.
캐시하기 위해서는 context가 반드시 동일한 configuration을 가져야한다.
@MockBean을 사용할때마다, context의 configuration은 변할 것이다.
음.. CI 시간을 무지막지하게 기다릴 수 있다면 사용하도록 하자
kotlin에서 테스트 코드를 보다보면서 lateinit이라는 키워드를 많이 보게 되었습니다.
@Autowired
private lateinit var redisTemplate: RedisTemplate<String, String>
private lateinit var redisService: RedisService
어디서 어렴풋이 성능에 더 좋기 때문에 사용한다는 말을 들었는데 (결론부터 말씀드리면 아닙니다!), 이에 대해 더 자세히 알고 싶어 공부해 본 내용을 추가로 정리해봅니다.
테스트에서 사용하는 이유
유연한 객체 초기화 : 각 테스트가 실행될 시점에 객체를 초기화 (주로 @Before 또는 beforeTest 메서드)
간결한 코드 : 이후 초기화되더라도 nullable 타입을 사용하지 않을 수 있음 (사용할 때 초기화가 강제되기 떄문)
성능 때문이 아님!! 초기화 시점을 제어할 수 있을 뿐, 메모리 할당이나 초기화 속도에서 이득이 없다.
즉 테스트에서 lateinit을 반드시 써야하는 것은 아니다!!
사용하면 좋은 때
지연 초기화가 필수적인 경우: 의존성 주입이 필요한 객체이지만, 테스트 환경에서 바로 초기화할 수 없거나 나중에 설정해야 하는 경우.
nullable을 피하고 싶은 경우: 변수 초기화를 나중으로 미루면서도 nullable 타입을 사용하고 싶지 않은 경우.
객체가 반드시 초기화되어야 하는 경우: 테스트에서 반드시 값이 주입되거나 초기화되어야 하지만, 그 시점은 테스트 실행 중간에 이루어질 경우
불필요한 때
주의 사항
lateinit 변수는 사용 전 반드시 초기화 되어야 함. 안하면 컴파일 단계에서UninitializedPropertyAccessException 발생
primitive type 안되는 이유? : lateinit 내부구현에 null과 비교하는 구현이 있는데, primitive type은 null 자체를 못가지기 때문에 이를 사용 불가
val이 안되는 이유? : lateinit은 초기화 지연하기 때문에 선언 동시 초기화하는 구문이 없어 이후 setter로 값을 넣줘야 하는데 val은 java 디컴파일시 setter 없음.
참조 : https://velog.io/@no1msh1217/Kotlin-lateinit-var%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC
: 둘다 늦은 초기화에 사용되는 키워드
늦은 초기화란?
var name: String? = null // NO!Lazy Initialization
Late Initialization
예시
fun main() {
lateinit var text: String
val textLength: Int by lazy {
text.length
}
text = "WEOLBU" // text 초기화됨. textLength도 따라서 초기화
println(textLength) // 6
text = "NEW_TEXT" // text는 바뀌었지만, textLength는 재계산되지 않음
println(textLength) // 여전히 6
}
만약 text가 바뀔 때마다 text.length도 바뀌게 하고 싶다면? → by Lazy 사용 X
fun main() {
lateinit var text: String
val textLength: Int
get() = text.length // 매번 text의 길이를 가져옴
text = "WEOLBU"
println(textLength) // 6
text = "NEW_TEXT"
println(textLength) // 8
}
좋은 내용 잘 보고 갑니다!!