본 글은 글쓴이의 개인적인 생각이 담겨있을 수 있습니다.
꼬리별 프로젝트 - Server Clematis
https://github.com/KKoRiByeol/Clematis
꼬리별이라는 개인 프로젝트를 시작하게 되면서
개인 프로젝트는 팀 프로젝트보다는 도전 부담이 적으니 하고 싶은 기술 다 써보고
테스트 코드도 잘 짜보자라는 생각을 하였다.
사건은 테스트 코드를 짜면서 발생했다.
테스트 코드는 다음과 같이 크게 네 가지로 나눌 수 있다.
그 중 컨트롤러 테스트는 통합 테스트와 겹치는 부분이 특히 많다고 생각해서 제외하였다.
이 중 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를 이용한 테스트 코드를 작성한다.
Service
클래스가 의존하고 있는 클래스들을mockk()
함수를 이용하여 Mocking 한다.Service
클래스가 의존하는 클래스 중every {}
를 이용하여 조작한다.every {}
를 사용하면서 매개변수로 필요한 값들을 필드로 정의한다.nonExistProject
, savedProject
등)Service
의 메소드를 실행하여 정상적으로 작동하는지 테스트한다.assertThat()
을, 에러가 발생한다면 assertThrows {}
를 이용하여verify {}
를 이용하여 예상한대로 Mocking한 메소드들이 작동했는지 테스트한다.