- [Kotlin] 'Kotlin'스러운 테스트 작성하기2025년 06월 10일
- 아몬드맛빼빼로
- 작성자
- 2025.06.10.:34
반응형
들어가며
사실 Kotlin 환경에서도 Java와 동일한 Mockito,JUnit 등을 유효하게사용할 수 있지만 'Kotlin'스럽게 테스트 코드 작성을 도와주는 여러 도구가 존재한다.
MockK
Kotlin을 위한 모킹 라이브러리로 Mockito에서 Kotlin 스타일로 만들어진 것이라 이해하면 쉽다. 코루틴이 지원되고 Private 메서드 역시 모킹이 가능하다. Kotlin 언어에서 지원하는 모든 것을 모킹할 수 있다.(ex: data class)
MockK는 Kotlin의 특성을 완전히 이해하고 설계된 모킹 라이브러리다. Mockito가 Java 리플렉션에 기반한 반면, MockK는 Kotlin의 메타데이터와 바이트코드 조작을 통해 더 강력한 모킹 기능을 제공한다.
특히 Kotlin의 final 클래스나 확장 함수도 모킹할 수 있으며, sealed class나 object 같은 Kotlin 고유 구조체들도 자연스럽게 처리한다. 또한 suspend 함수를 포함한 코루틴 기반 코드의 테스트도 간편하게 작성할 수 있어, 비동기 로직이 많은 현대적인 Kotlin 애플리케이션에서 필수적인 도구라고 할 수 있다.
MockK의 DSL은 Kotlin 개발자에게 친숙한 방식으로 설계되어 있어,every { ... } returns ...와 같은 직관적인 문법으로 모킹 동작을 정의할 수 있다. 이는 Mockito의 when(...).thenReturn(...)보다 더 자연스럽고 읽기 쉬운 코드를 만들어낸다.
Kotest
JUnit은 훌륭한 테스트 도구이지만 Kotest도 사용할 수 있다. Kotlin DSL을 이용하여 더 Kotlin스러운 테스트가 작성이 가능하다. 다양한 코드 스타일을 지원하는데 특히 Given-When-Then 스타일을 사용할 수 있도록 Behavior Spec을 제공해준다.
Kotest는 단순히 JUnit의 대안이 아니라, Kotlin 생태계에 최적화된 종합적인 테스트 프레임워크다. 10가지 이상의 다양한 테스트 스타일(Spec)을 제공하여 개발자가 상황에 맞는 최적의 테스트 작성 방식을 선택할 수 있다.
주요 Spec 종류:
- BehaviorSpec: BDD 스타일의 Given-When-Then 패턴
- StringSpec: 가장 간단한 문자열 기반 테스트
- FunSpec: 함수형 스타일의 테스트
- DescribeSpec: RSpec 스타일의 중첩 구조
- ShouldSpec: 자연어에 가까운 should 문법
Kotest의 강력한 assertion 라이브러리는 shouldBe, shouldContain, shouldThrow 등의 직관적인 매처(matcher)를 제공하여 JUnit의 assertEquals보다 훨씬 읽기 쉬운 테스트 코드를 작성할 수 있다. 또한 property-based testing, data-driven testing 등의 고급 테스트 기법도 내장하고 있어 더 견고한 테스트를 작성할 수 있다.
무엇보다 Kotlin의 언어적 특성을 완전히 활용하여 테스트 코드 자체가 하나의 DSL처럼 동작한다는 점이 가장 큰 장점이다.
더보기Behavior Spec?
Behavior Spec은 소프트웨어 개발에서 행동(Behavior)을 중심으로 시스템 요구사항이나 테스트를 명세하는 방식이다. 보통은 Behavior Specification 또는 Behavior-Driven Development(BDD)의 한 형태로 사용되며, 시스템이 어떻게 행동해야 하는지(Should do)를 자연어 형태로 서술하는 것이다.
예제
예제를 보기에 앞서 테스트 코드를 작성하는 방법은 무척이나 다양하며 이는 프로젝트마다 협의 하에 정해야 하는 것으로 이 예제처럼 Kotlin테스트를 만들어야 하는 것은 아니다.여기선 Given-When-Then 패턴의 BDD를 기반으로 한 스타일을 적용한다.
class BookLoanUseCaseTest: BehaviorSpec({ val bookRepository = mockk<BookRepository>() val userRepository = mockk<UserRepository>() val loanRepository = mockk<LoanRepository>() val bookLoanUseCase = BookLoanUseCase(bookRepository, userRepository, loanRepository) Given("유효한 도서 대출 요청이 주어졌을 때") { val userId = 1L val bookId = 100L val loanRequest = LoanRequest(userId, bookId) val user = User(id = userId, name = "김철수") val book = Book(id = bookId, title = "클린코드", available = true) val savedLoan = Loan(id = 1L, userId = userId, bookId = bookId) every { userRepository.findById(userId) } returns user every { bookRepository.findById(bookId) } returns book every { loanRepository.save(any()) } returns savedLoan When("도서를 대출하면") { val result = bookLoanUseCase.loanBook(loanRequest) Then("대출 기록이 저장되어야 한다") { verify(exactly = 1) { loanRepository.save(any()) } } Then("대출 ID가 반환되어야 한다") { result.loanId shouldBe savedLoan.id } } When("이미 대출된 도서를 대출하려 하면") { every { bookRepository.findById(bookId) } returns book.copy(available = false) Then("BookNotAvailableException이 발생해야 한다") { shouldThrow<BookNotAvailableException> { bookLoanUseCase.loanBook(loanRequest) } } } When("존재하지 않는 사용자가 대출하려 하면") { every { userRepository.findById(userId) } returns null Then("UserNotFoundException이 발생해야 한다") { shouldThrow<UserNotFoundException> { bookLoanUseCase.loanBook(loanRequest) } } } } })Given (주어진 상황에서)
테스트의 초기 상태를 설정한다. 여기서는 테스트에 필요한 객체들을 Mock(가짜 객체)으로 생성하고, 필요한 입력값과 초기 설정을 준비한다. 예를 들어, BookLoanRequest 객체에 사용자 ID와 도서 ID를 설정하고, 가짜 userRepository와 bookRepository가 특정 값들에 대해 예상된 결과를 반환하도록 설정하여, 해당 사용자와 도서가 존재하며 대출 가능한 상태라고 가정한다.
When (특정 조건이 실행될 때)
실제로 테스트하고자 하는 동작이나 메서드를 실행한다. 이 경우에는 bookLoanUseCase.execute(loanRequest) 메서드를 호출하여, 주어진 BookLoanRequest를 사용해서 도서 대출 요청을 시뮬레이션한다.
Then (그 결과로 기대되는 것)
When 절에서 실행된 동작의 결과를 검증한다. 기대하는 결과가 실제와 일치하는지 확인하여, 테스트의 성공 여부를 결정한다. 첫 번째 When-Then 구조에서는 loanRepository.save(any())가 정확히 한 번 호출되었는지를 검증하여, Loan 객체가 저장되었는지 확인한다. 두 번째 When-Then 구조에서는 이미 대출된 도서로 대출을 시도할 경우 BookNotAvailableException 예외가 발생하는지를 검증한다.
마무리
Kotlin 환경에서 MockK와 Kotest를 활용한 테스트 작성은 기존 Java 생태계의 테스트 도구들보다 더욱 Kotlin다운 방식으로 테스트 코드를 작성할 수 있게 해준다. MockK의 직관적인 DSL과 Kotest의 다양한 테스트 스타일 지원을 통해 개발자는 더 표현력 있고 읽기 쉬운 테스트 코드를 작성할 수 있다.
특히 Kotlin의 언어적 특성들(확장 함수, 널 안전성, 데이터 클래스 등)을 완전히 활용할 수 있다는 점에서 큰 장점이 있다. 또한 코루틴과 같은 Kotlin 고유 기능들도 자연스럽게 테스트할 수 있어, Kotlin으로 작성된 애플리케이션의 모든 부분을 효과적으로 검증할 수 있다.
물론 기존의 JUnit과 Mockito 조합도 충분히 훌륭한 선택이다.하지만 Kotlin의 철학과 문법에 더 잘 맞는 테스트 도구들을 사용함으로써 일관성 있는 코드베이스를 유지하고, 개발 생산성을 더욱 향상시킬 수 있다. 팀의 상황과 프로젝트 요구사항에 맞춰 적절한 도구를 선택하되, Kotlin의 강력함을 테스트 코드에서도 충분히 활용해보면 좋을 것이다.
'Kotlin' 카테고리의 다른 글
[Kotlin] BuildSrc (0) 2025.08.29 다음글이전글이전 글이 없습니다.댓글