쟈미로그

Kotest와 MockK을 찍먹해보자!~ 본문

Test

Kotest와 MockK을 찍먹해보자!~

쟈미 2023. 8. 15. 23:39

요즘 인생 처음으로 코틀린을 사용한 서버 개발을 하고 있다.

테스트 코드의 필요성을 느끼던 찰나, 한 팀원분이 Kotest vs JUnit 중 어떤 프레임워크를 사용할지 정하자고 하셨다. 자바 스프링부트로 개발할 때는 JUnit+Mockito로 단위 테스트를 짰었고, 그래서 큰 생각 없이 이 조합으로 테스트코드를 짜려했는데 오산이었다. (사실 Kotest라는 테스트 프레임워크가 있는 줄도 몰랐다 히히)

그래서 생각해보니 자바와 유사한 코틀린이지만 사용해볼 수록 자바 스타일 != 코틀린 스타일이긴 했다. 테스트 프레임워크도 코틀린에 어울리는 게 있다면 JUnit보단 그걸 사용하는게 맞다는 생각이 들었고, 찾아보니 Kotest가 마침 '코틀린에 어울리는 테스트 프레임워크'였다.

 

그래서 지금부터 Kotest 프레임워크와 MockK 라이브러리를 활용해서 코틀린 단위 테스트 코드를 맛보기해보겠다.

코틀린 강의 들으면서 짠 간단한 코드들을 Kotest로 테스트해볼 예정.

 

 

Testing Styles

Kotest는 10개의 테스트 레이아웃을 제공해줘서 원하는 스타일대로 편하게 테스트 코드를 짤 수 있도록 해준다! 레이아웃들은 추상클래스로 제공돼서 상속하여 진행하는 형태다.

레이아웃은 기존의 다른 테스트 프레임워크에서 영감받아 만들어진 것도 있고, Kotest만을 위해 만들어진 것도 있다고 한다. 자세한 것은 문서 참고.

 

나는 익숙하기도 하고 Service 계층 테스트에 어울리는 Given-When-Then BDD 방식으로 짜볼 것이다.

BDD 스타일은 Behavior Spec 추상클래스를 구현하면 된다.

 

레이아웃 추상클래스

참고로 이 레이아웃 클래스들 보면 별도의 추상메소드가 없고 테스팅 스타일을 위해 제공하는 확장함수 정도만 있었다.

레이아웃 추상클래스를 쓸 때 특이하다고 느낀 점은, 테스트 코드를 추상클래스 생성자에 작성한다는 점이었다. 

abstract class BehaviorSpec(body: BehaviorSpec.() -> Unit = {}) : DslDrivenSpec(), BehaviorSpecRootScope {
   init {
      body()
   }
   ...
}

이 추상클래스들은 기본생성자에 람다식 파라미터가 있다! 그래서 테스트 코드를 생성자에 작성해야한다.

 

 

단위 테스트

본격적으로 Kotest+MockK를 사용해봤다.

 

Kotest

Given-When-Then 구문을 메소드 코드블락으로 나타낼 수 있다는 점에서 직관적이다.

그리고 문자열로 행위를 명시할 수 있다는 점도 큰 장점이라고 느껴졌다.

Given("ID 1의 유저 A가 있는 상황에서") {
    ...
    When("유저 A가 자신의 이름을 B로 변경을 요청하면") {
		...

        Then("사용자 조회를 해야한다.") {
			...
        }
        Then("ID 1로 재조회 시 이름이 B로 변경돼야한다.") {
			...
        }
    }
}

 

  • Given, When, Then

참고로 Given, When, Then 메소드 타고 들어가보면, 살짝 다른 이름으로 똑같은 기능의 메소드들도 함께 제공하고 있다.

suspend fun When(name: String, test: suspend BehaviorSpecWhenContainerScope.() -> Unit) = addWhen(name, test, xdisabled = false)
suspend fun `when`(name: String, test: suspend BehaviorSpecWhenContainerScope.() -> Unit) = addWhen(name, test, xdisabled = false)

 

When을 예시로 보면, 이렇게 When과 'when' 둘 다 제공하고 있다. Given, Then도 마찬가지로 given, then을 함께 제공하고 있음.

다양한 표기로 코드를 짤 수 있도록 하는 것 같다. 이건 팀원들과 상의해서 정하면 될 부분.

 

MockK

  • Mocking

MockK : 목킹할 객체에 붙이는 어노테이션

InjectMockKs : MockK 어노테이션이 붙은 객체들을 주입받을 수 있는 어노테이션

@MockK
lateinit var userRepository: UserRepository

@InjectMockKs
lateinit var userService: UserService

 

  • Stubbing

Mockito에서는 given().willReturn() 메소드를 사용해서 목 객체의 동작을 정의했었다.

MockK에서는 every라는 메소드를 통해 코드블록으로 subbing 할 수 있었다.

every { // stub
    userRepository.findByIdOrThrow(beforeUser.id!!)
}.returns(beforeUser)

 

  • Verify

객체 동작 검증을 위해 verify 메소드를 제공한다.

메소드 블락에다 검증할 코드를 명시하고, 메소드 파라미터에서 최소 실행 수, 최대 실행 수 등 검증할 값들을 받는다.

verify(exactly = 1) {
    userRepository.findByIdOrThrow(beforeUser.id!!)
}

 

전체 코드

사실 코드가 너무 별 것 없는 CRUD 코드들이었어서.. 목킹이나 스터빙할 필요가 전혀 없는 코드들이었지만 Kotest 사용에 의의를 두었다.

억지로 짜낸 테스트 코드지만 일단 전문 첨부..히히

Given("ID 1의 유저 A가 있는 상황에서") {
    val beforeUser = User(name = "A", age = null, id = 1)
    val request = UserUpdateRequest(beforeUser.id!!, "B")

    When("유저 A가 자신의 이름을 B로 변경을 요청하면") {
        userService.updateUserName(request)
        every { // stub
            userRepository.findByIdOrThrow(beforeUser.id!!)
        }.returns(beforeUser)

        Then("사용자 조회를 해야한다.") {
            verify(exactly = 1) {
                userRepository.findByIdOrThrow(beforeUser.id!!)
            }
        }
        Then("ID 1로 재조회 시 이름이 B로 변경돼야한다.") {
            // TODO
        }
    }
}

 

 

사용 후기

일단 Kotest, MockK 공식 문서로는 레퍼런스만 보고, 얘네의 특징이나 장점을 제대로 보진 않은 상태에서 사용 후기를 적어보자면

  • 어노테이션 덕지덕지 테스트 코드에서 벗어날 수 있었음
  • 레이아웃 제공 덕분에 가독성 좋게 테스트 코드를 짤 수 있었음. JUnit에선 거의 주석으로 적었는데!

등등 있는 듯함!

진짜 맛보기로 메소드 몇개만 테스트 짜본 건데도 장점이 뚜렷했다. 확실히 코틀린에는 Kotest+MockK 쓰라는 이유가 있구나 싶었다. 더 공부해보면 장단점을 제대로 알 수 있을 것 같음ㅎㅎ

이제 찍먹해봤으니 Kotest 공식문서 좀 읽고 Mockito vs MockK도 알아봐야겠다~!!

Comments