어떤 기능을 구현한다는 것은 테스트까지 작성이 되어야 완성이라고 이야기할 수 있습니다. 그만큼 테스트는 중요한데요. 이번 시간에는 JUnit5 기반으로 단위 테스트를 작성하고 코틀린 스타일로 Mock 객체를 만들 수 있는 MockK 프레임워크를 사용해볼 예정입니다.
Project issue: https://github.com/kdohyeon/crypto-labs/issues/15
Pull request: https://github.com/kdohyeon/crypto-labs/pull/16
의존성 추가
단위 테스트 작성을 위해서는 spring-boot-starter-test 모듈을 불러온다. build.gradle.kts 에 추가해주면 된다.
testImplementation("org.springframework.boot:spring-boot-starter-test")
MockK 관련 라이브러리도 추가해주자.
testImplementation("io.mockk:mockk:1.12.5")
테스트 작성
테스트를 하기 위한 메소드는 마켓을 개별로 조회하는 marketSearchService.marketPair 에 대해 작성하려고 한다. DB에서 조회를 하고 값이 없으면 IllegalArgumentException 예외를 던진다. 값이 존재한다면 MarketDto 로 변환을 하여 반환한다.
@Service
class MarketSearchService(
private val marketRepository: MarketRepository,
private val marketDtoConverter: MarketDtoConverter,
) : SearchMarketUseCase {
override fun marketPair(marketPair: String): MarketDto {
return marketRepository.findByMarketPair(marketPair)
.orElseThrow { throw IllegalArgumentException("마켓 [$marketPair] 은 존재하지 않습니다.") }
.let { marketDtoConverter.convert(it) }
}
}
테스트를 하고 싶은 부분은 크게 2가지가 존재한다.
1) DB에서 성공적으로 조회를 했을 경우, 그리고
2) 값이 없어 조회에 실패한 경우이다.
각각에 대해 테스트를 해보자.
MockK
테스트를 수행할 때 직접적으로 DB와 연결을 하지 않을 것이기 때문에 MarketRepository 는 mocking 이 필요하다. Mocking 이라는 것은 해당 객체가 있다고 가정하고 테스트를 진행하는 것이다. MarketRepository 객체가 만들어져 있다고 가정하고 그 다음 코드를 진행한다. (이 테스트에서는 MarketRepository 가 중요한 것이 아니기 때문에 Mocking 처리한다.)
Mocking 은 Mockito 를 통해 할 수도 있지만 코틀린에서는 MockK 를 많이 사용한다.
Test #1. 성공 케이스
(1) MockK 의 Mock 객체를 사용하기 위해 클래스 레벨에 @ExtendWith 어노테이션으로 선언한다. (2) MockK 어노테이션을 통해 MarketRepository 를 Mocking 한다고 선언한다. (3) SpyK 어노테이션을 통해 실 객체를 사용하겠다고 선언한다 (no mocking). (4) InjectMockKs 어노테이션은 Mock 객체를 주입한다. MarketSearchService 테스트를 수행할 때 선언한 Mock 객체를 주입한다.
(5) 각 단위 테스트를 선언하기 위한 어노테이션이고 DisplayName 으로 어떤 테스트인지 작성할 수 있다.
(6) 단위 테스트는 주로 given-when-then 패턴으로 작성되는데, given 에서는 주어진 값을 정의한다. marketPair 와 market 데이터가 테스트에 필요하기 때문에 미리 작성해둔다. 또한 (7) marketRepository 를 mocking 했기 때문에 marketRepository의 findByMarketPair(marketPair) 가 호출되었을 때 어떤 값을 기대할 지 정의할 수 있다.
(8) 테스트를 하고자 하는 메소드를 실행시킨다.
(9) 테스트가 성공적으로 수행되었는지를 확인하기 위한 선언문들을 작성한다. verify 를 통해 marketRepository의 findByMarketPair 메소드가 한 번 수행되어야 한다고 선언한다. 또한 marketDto.marketPair 의 값과 marketPair 의 값이 동일해야 한다고 선언한다.
@ExtendWith(MockKExtension::class) // (1)
internal class MarketSearchServiceTest {
@MockK // (2)
lateinit var marketRepository: MarketRepository
@SpyK // (3)
var marketDtoConverter = MarketDtoConverter()
@InjectMockKs // (4)
lateinit var marketSearchService: MarketSearchService
@Test // (5)
@DisplayName("마켓을 개별로 조회할 수 있다.")
fun marketPairOnSuccess() {
// given (6)
val marketPair = "KRW-BTC"
val market = Market(
marketPair = marketPair,
koreanName = "비트코인",
englishName = "Bitcoin",
marketWarning = false,
)
// (7)
every { marketRepository.findByMarketPair(marketPair) } returns Optional.of(market)
// when (8)
val marketDto = marketSearchService.marketPair(marketPair)
// then (9)
verify(exactly = 1) { marketRepository.findByMarketPair(marketPair) }
assertEquals(marketPair, marketDto.marketPair)
}
}
Test #2. 실패 케이스
나머지 부분은 위에 설명한 것과 동일한데, 예외의 경우에는 assertThrows 를 활용할 수 있다. 예외 자체를 기대하는 것이고, 예외가 발생해야 성공이라고 보는 것이다. marketSearchService.marketPair(marketPair) 가 수행될 때 IllegalArgumentException 이 발생되어야 하고 에러 메시지도 같이 체크할 수 있다.
@ExtendWith(MockKExtension::class)
internal class MarketSearchServiceTest {
@MockK
lateinit var marketRepository: MarketRepository
@SpyK
var marketDtoConverter = MarketDtoConverter()
@InjectMockKs
lateinit var marketSearchService: MarketSearchService
@Test
@DisplayName("마켓을 개별로 조회할 때, 결과가 없으면 예외가 발생한다.")
fun marketPairOnFailure() {
// given
val marketPair = "KRW-BTC"
every { marketRepository.findByMarketPair(marketPair) } returns Optional.empty()
// when
val exception = assertThrows(IllegalArgumentException::class.java) {
marketSearchService.marketPair(marketPair)
}
// then
verify(exactly = 1) { marketRepository.findByMarketPair(marketPair) }
assertEquals("마켓 [$marketPair] 은 존재하지 않습니다.", exception.message)
}
}
'스프링 > 만들면서 배우는 실무 백엔드 개발' 카테고리의 다른 글
11. Jacoco + Gradle.kts 로 테스트 커버리지 확인하기 (feat. SonarQube) (0) | 2023.04.07 |
---|---|
10. ArchUnit 으로 아키텍쳐 검사하기 (0) | 2023.04.05 |
8. JPA 적용하기 (0) | 2023.03.30 |
7. 스프링부트에서 H2 데이터베이스 연결하기 (0) | 2023.03.29 |
6. buildSrc 기반으로 라이브러리 버전 관리하기 (0) | 2023.03.27 |
댓글