Kotlin Extension은 어떻게 테스트 해?

Jaychy·2021년 4월 15일
0

프로젝트 꼬리별

목록 보기
4/4
post-thumbnail

본 글은 글쓴이의 개인적인 생각이 담겨있을 수 있습니다.

꼬리별 프로젝트 - Server Clematis
https://github.com/KKoRiByeol/Clematis

사건의 발단

꼬리별이라는 개인 프로젝트를 시작하게 되면서
개인 프로젝트는 팀 프로젝트보다는 도전 부담이 적으니 하고 싶은 기술 다 써보고
테스트 코드도 잘 짜보자라는 생각을 하였다.

사건은 테스트 코드를 짜면서 발생했다.
테스트 코드는 다음과 같이 크게 네 가지로 나눌 수 있다.

  • Controller Layer Test
  • Service Layer Test
  • Repository Layer Test
  • Integration Test

그 중 컨트롤러 테스트는 통합 테스트와 겹치는 부분이 특히 많다고 생각해서 제외하였다.

이 중 Service Layer Test를 하는 도중 사건이 발생하였다.
Integration Test의 경우에는 모든 경로를 테스트하는 것이므로
실제 서비스가 동작한다면 크게 문제 될 것이 없었고,
Repository Layer Test의 경우에는 가장 끝단에 위치하기 때문에
의존성과 관련되어 문제 생길 것이 없었다.

하지만 Service Layer Test는 의존하는 Repository를 Mocking 해서
독립적으로 테스트를 하도록 하는 과정이 필요하므로
Mocking 라이브러리인 Mockito를 사용하게 되었다.

문제

본격적인 문제는 Mockito를 이용해 확장 함수를 Mocking 할 때부터 발생한다.
Mockito는 더욱 코틀린 다운 라이브러리를 제공하기 위해서
Mockito-Kotlin 이라는 라이브러리를 제공한다.

[Mockito-Kotlin] https://github.com/mockito/mockito-kotlin
이 외에도 Mockito-Scala 와 같은 라이브러리도 제공한다.

내가 Mocking 하려고 했던 Repository의 메소드는 findByIdOrNull() 메소드이다.
findByIdOrNull() 메소드는 코틀린의 확장 함수로써,
단순히 findById().orElse(null) 의 shortcut 이다.

Mockito-Kotlin은 정적(Static)인 함수도 Mocking할 수 있도록
mock() 함수 대신에 staticMock() 이라는 함수도 제공한다.
코틀린 확장 함수 (Kotlin Extension Function)는 내부적으로는 Static하게 구현되어 있기에
이를 이용하면 해결할 수 있을 것이라고 생각했다.

하지만 당연하게도 오류가 발생하였다.
그래서 나와 같은 문제를 인지한 사람이 많을 것이라고 생각해서
구글링을 해보니 당연하게도 많은 사람들이 궁금함을 자아내고 있었다.
그 중 나는 Mockito-Kotlin의 Issue에서 다음과 같은 답변을 들을 수 있었다.

[Mockito-Kotlin Issue] https://github.com/mockito/mockito-kotlin/issues/198

"아마도 Mockito-Kotlin의 공식적인 답변은
Mockito-Kotlin으로 Kotlin Extension Function을 Mocking 하지 말라일 것이다."

해결

Mockito-Kotlin에서 Kotlin Extension Function Mocking을 지원하지 않자,
사람들은 MockK라는 라이브러리를 만들었다.

본 라이브러리는 더욱 코틀린 다운 Mocking을 지원하고,
리플렉션을 통해 메소드의 매개변수에 따른 리턴 값을 정의하기 때문에
기존 Mockito에서 지원하지 않는 Kotlin Extension Function에 대한 Mocking도 지원한다.

그런데 이 MockK를 이용하여 테스트 코드를 짜는 방식은 사람마다 다를 것이다.
무엇이 정답인지는 나도 아직 잘 모르겠지만 계속 발전하고자 한다면
더 나은 방법을 찾을지도 모른다.
다음은 꼬리별 프로젝트에 사용한 ProjectModificationService의 테스트 코드이다.

internal class ProjectModificationServiceTest {
    private val projectRepository = mockk<ProjectRepository>()
    private val testService = ProjectModificationService(
        projectRepository = projectRepository,
    )

    private val savedAccount = Account(
        id = "savedIdId",
        password = "savedPassword",
        name = "savedName",
    )
    private val savedProject = Project(
        code = "savedProject-finally",
        name = "savedProject",
        description = "savedDescription",
        owner = savedAccount,
    )
    private val nonExistProject = Project(
        code = "nonExistProject-finally",
        name = "nonExistProject",
        description = "nonExistDescription",
        owner = savedAccount
    )

    @Test
    fun `프로젝트 내용 수정하기`() {
        every { projectRepository.findByIdOrNull(savedProject.code) } returns savedProject
        every { projectRepository.findByIdOrNull(nonExistProject.code) } returns null

        testService.modifyProject(
            projectCode = "savedProject-finally",
            newProjectName = "newProject",
            newProjectDescription = "newDescription",
        )

        verify(exactly = 1) { projectRepository.findByIdOrNull(savedProject.code) }
    }

    @Test
    fun `프로젝트 내용 수정하기 - throw ProjectNotFoundException`() {
        every { projectRepository.findByIdOrNull(savedProject.code) } returns savedProject
        every { projectRepository.findByIdOrNull(nonExistProject.code) } returns null

        assertThrows<ProjectNotFoundException> {
            testService.modifyProject(
                projectCode = "nonExistProject-finally",
                newProjectName = "newProject",
                newProjectDescription = "newDescription",
            )
        }

        verify(exactly = 1) { projectRepository.findByIdOrNull(nonExistProject.code) }
    }
}

나는 다음과 같은 순서로 MockK를 이용한 테스트 코드를 작성한다.

  1. 테스트의 대상인 Service 클래스가 의존하고 있는 클래스들을
    mockk() 함수를 이용하여 Mocking 한다.
  2. 대상 Service 클래스가 의존하는 클래스 중
    사용되는 함수를 every {}를 이용하여 조작한다.
  3. every {}를 사용하면서 매개변수로 필요한 값들을 필드로 정의한다.
    (nonExistProject, savedProject 등)
  4. Service의 메소드를 실행하여 정상적으로 작동하는지 테스트한다.
  5. 리턴 값이 있다면 assertThat()을, 에러가 발생한다면 assertThrows {}를 이용하여
    테스트의 결과도 테스트한다.
  6. verify {}를 이용하여 예상한대로 Mocking한 메소드들이 작동했는지 테스트한다.
profile
아름다운 코드를 꿈꾸는 백엔드 주니어 개발자입니다.

0개의 댓글