테스트가 어려웠던 이유 - 종속성 관리
별로 어렵지 않은 경우 바로 아래로 가시면 됩니다.
나의 경우 테스트를 선뜻 시작하기 어려웠던게 mocking이 가능하다는걸 몰랐기 때문이었다.
아래처럼 HiService라는 복잡한 클래스가 있다고 해보자.
유저 이름을 넣으면 그 이름을 넣은 인사카드 gif 이미지를 반환하는 그런 서비스다.
class HiService(
// 특징 : DB에서 UserEntity를 불러온다.
private val userRepository: UserRepository,
// 특징 : UtilBean에 method가 백개쯤 있음 곧 분리될 예정
private val utilBean: UtilBean,
// 특징 : API 호출 건수대로 돈받는 유료 서비스
private val assetRepository: AssetRepository,
private val animateRepository: AnimateRepository,
) {
fun hi(userName: Sring): Response<HiAsset> {
val region = utilBean.verifyRegion(userName)
val user: UserEntity? = userRepository(userName)
if (user == null) { throw CustomExeption("...") }
val image = assetRepository.getHiImage()
val animateType = animateRepository.jump()
return Response.ok(
utilBean.combinate(image, animateType)
)
}
}
JUnit 가이드만 좀 읽어보고 @Autowired를 사용해서 이 서비스를 테스트해보려고 하면 해야할게 너무나도 많다.
- userRepository는 DB랑 연결되어야하니 별도 테스트 DB를 만들어야 기존 데이터 영향 없이 테스트를 수행할 수 있다.
- assetRepository에서 신규자원을 가져오면 요금이 징수되서 선뜻 호출하기가 어렵다
- 사실 HiService를 사용하는 상위 모듈의 테스트에서도 다 검증하고있는 작업이다. ex) HiController 등
이런저런 생각을 하다보면 시간 없으니 일단 QA랑 통합테스트로 퉁치자는 생각이 든다.
시간이 지나도 단위테스트는 영영 작성되지 않고 기능 하나 바꿀 때마다 야근을 하게된다...
이런 고민을 해결해줄 수 있는게 바로 모킹 프레임워크이다.
파이썬이나 자바스크립트에서 수행하는 테스트랑 느낌이 비슷하다.
모듈에서 사용할 메서드의 리턴값만 정의해주면 테스트 준비가 끝난다.
테스트 DB, 외부 API 연결 다 필요 없다. 하위 모듈의 로직 에러로부터 영향을 받을 이유도 없다.
모든 레이어에 대해 단위 테스트가 작성되었다는 전제하에, 적은 비용과 짧은 시간으로 기능을 검증할 수 있어서 통합테스트보다 유리하다.
Mockito-kotlin 종속성 설정
Spring + Kotlin을 사용하는 경우 아래 종속성에 mockito가 포함되어있다.
// Kotlin
testImplementation("org.springframework.boot:spring-boot-starter-test")
// Groovy
testImplementation "org.springframework.boot:spring-boot-starter-test"
mockito-kotlin 라이브러리가 특별히 필요한 경우 아래처럼 받아오면 된다.
주로 null 관련 오류때문에 해당 라이브러리가 필요하게된다.
// Kotlin
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
// Groovy
testImplementation "org.mockito.kotlin:mockito-kotlin:5.4.0"
Service 테스트 작성 예시
repository 하나를 주입받는 아래와 같은 Service 클래스를 테스트한다고 생각해보자.
class HiService(private val userRepository: UserRepository) {
fun welcome(userName: String): String { ... }
}
단위테스트이므로 클래스 단위에 주석을 달아줄 필요는 없다. 먼저 테스트 내용을 함수명으로 표현해주자.
class HiServiceTest {
@Test
fun `유저가 없으면 가입 안내 메세지를 반환한다`()
@Test
fun `유저가 있으면 환영 메세지를 반환한다`()
}
이대로 돌려보면 테스트가 실패하질 않으니 둘다 통과한다고 나온다.
이제 HiService에서 사용할 가짜 UserRepository를 만들어주자.
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
// ...
val userRepository = mock(UserRepository::class.java)
`when`(userRepository.findUser("김아무개")).thenReturn(False)
// Mock 객체 반환값 비교
println(userRepository.findUser("김아무개")) // False (설정값)
println(userRepository.findUser("박아무개")) // True (기본값)
println(userRepository.getInt()) // 출력 : 0
println(userRepository.getList()) // 출력 : []
`when` & thenReturn 으로 설정하지 않은 메서드에 대해서는 타입만 맞춘 기본값들이 나온다.
따라서 정확히 원하는 값이 있는 경우에만 메서드 리턴값을 정의해주면 된다.
이제 이러한 기능을 활용해서 테스트 DB 없이 Service 로직을 검증할 수 있다.
class HiServiceTest {
private val userRepository = mock(UserRepository::class.java)
private val hiService = HiService(userRepository)
// ...
@Test
fun `유저가 있으면 환영 메세지를 반환한다`() {
val givenUser = "김아무개"
`when`(userRepository.findUser("김아무개")).thenReturn(False)
assert(hiService.welcome("김아무개") == "김아무개님 환영합니다.")
}
}
@BeforeEach 같은 주석으로 예전 모킹 정보를 지워야하지 않나 싶겠지만,
mockito에서 똑똑하게 예전 테스트 코드에서 등록된 동작은 다음 테스트에서 지워버린다.
그리고 단순 assert에 더해서, Mock 객체의 어떤 메서드가 호출되었는지 확인할 때 verify 메서드를 사용할 수 있다.
import org.mockito.Mockito.verify
import org.mockito.kotlin.any as kotlinAny
verify(`mock객체`).`확인 원하는 메서드`(`원하는 인풋`) // times(1)이 기본 적용됨
verify(`mock객체`, atMost(3)).`최대 3번 호출`(`원하는 인풋`)
verify(`mock객체`, atLeast(3)).`최소 3번 호출`(`원하는 인풋`)
verify(`mock객체`, times(3)).`정확히 3번 호출`(`원하는 인풋`)
verify(`mock객체`, never()).`호출 안함`(kotlinAny<`인풋타입`>())
HiService같은 경우 유저 이름을 여러번 검색할 필요가 전혀 없으니, 유저 검색이 한번만 이루어졌는지 검증하는 테스트 코드를 추가해보자. 아래처럼 검사할 수 있다.
@Test
fun `유저 검색은 한번만 수행한다`() {
val givenUser = "김아무개"
`when`(userRepository.findUser("김아무개")).thenReturn(False)
assert(hiService.welcome("김아무개") == "김아무개님 환영합니다.")
verify(userRepository, times(1)).findUser("김아무개")
}
이처럼 `when`, thenReturn 및 verify만 사용할 줄 알아도 이제 조금의 검색으로 단위테스트를 작성할 수 있게 될 것이다.
참고자료
- Baeldung | Mockito + Kotlin 가이드
'프로그래밍 언어 > Kotlin' 카테고리의 다른 글
속성값으로 enum 객체 얻기 (value to enum) (0) | 2023.01.06 |
---|---|
Kotlin의 ArrayDequeue : Stack & Queue를 손쉽게 사용하기 (1) | 2023.01.06 |
Kotlin의 시간계산 : LocalDate, LocalDateTime, Duration (0) | 2022.11.27 |
List를 Map으로 만들기: associate (0) | 2022.11.26 |
java.io.File 사용법 (0) | 2022.11.06 |