Kotest 통합(Intergrations)

Kotest와 모킹 프레임워크(mockk) 및 JaCoCo를 함께 사용하여 좀 더 견고하고 품질 높은 테스트를 작성할 수 있다. 여기에서는 테스트의 격리와 의존성 관리를 도와주는 mockk과 코드 커버리지 도구로, 코드베이스에서 얼마나 많은 부분이 테스트되었는지를 측정할 수 있는 JaCoCo에 대해서 설명한다.

Mocking과 Kotest

Kotest 자체에는 모의 테스트 기능이 없다. 그러나 선호하는 모의고사 라이브러리를 쉽게 플러그인할 수 있다.

예를 들어, mockk를 살펴보자:

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }
})

이 예제는 예상대로 작동하지만 해당 mockk를 사용하는 테스트를 더 추가하면 어떻게 될까?

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

    test("Saves to repository as well") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

})

위의 스니펫은 예외를 발생시킨다!

2 matching calls found, but needs at least 1 and at most 1 calls

호출 사이에 모의 테스트가 다시 시작되지 않기 때문에 이런 일이 발생한다. 기본적으로 Kotest는 실행할 모든 테스트에 대해 사양의 단일 인스턴스를 생성하여 테스트를 격리한다.

이로 인해 모의가 재사용된다. 이 문제를 어떻게 해결할 수 있을까?

옵션 1 - 테스트 전 모의 설정

class MyTest : FunSpec({

    lateinit var repository: MyRepository
    lateinit var target: MyService

    beforeTest {
        repository = mockk()
        target = MyService(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

옵션 2 - 테스트 후 모의고사 재설정

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    afterTest {
        clearMocks(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

리스너 위치 지정하기

사양 정의 내에서 실행되는 모든 함수는 끝에 리스너를 위치시킬 수 있다.

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    afterTest {
        clearMocks(repository)  // <---- End of file, better readability
    }

})

옵션 3 - 격리 모드 조정하기

사용 용도에 따라 특정 사양의 격리 모드를 조정하는 것도 좋은 방법일 수 있다. 격리 모드에 대해 더 자세히 알고 싶다면 격리 모드 문서를 참조하라.

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)


    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    isolationMode = IsolationMode.InstancePerTest

})

JaCoCo

Kotest는 표준 Gradle 방식으로 코드 커버리지를 위해 JaCoCo와 통합된다. Gradle 설치 지침은 여기에서 확인할 수 있다.

  1. Gradle에서 플러그인에 jacoco를 추가한다.
    plugins {
        ...
        jacoco
        ...
    }
    
  2. jacoco 구성
    jacoco {
        toolVersion = "0.8.11"
        reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional
    }
    
  3. jacoco XML 보고서 작업을 추가한다.
    tasks.jacocoTestReport {
        dependsOn(tasks.test)
        reports {
            xml.required.set(true)
        }
    }
    
  4. 테스트 작업을 jacoco코에 종속되도록 변경한다.
    tasks.test {
        ...
        finalizedBy(tasks.jacocoTestReport)
    }
    

이제 test를 실행하면 Jacoco 보고서 파일이 $buildDir/reports/jacoco에 생성된다.

Note: 멀티 모듈 프로젝트인 경우 각 서브모듈에 jacoco 플러그인을 적용해야 할 수도 있다.


참조




최종 수정 : 2024-04-23