Mockk 등의 라이브러리 대신 직접 Stub 객체를 만들어서 테스트 코드를 개선한 경험을 공유하고자 한다. 참고로 지정한 응답을 주는 객체를 의미한다.
기존 레거시 코드를 리팩토링 하기 전에 앞서 단위 테스트를 먼저 작성하고 있었고, 이때 의존성을 테스트 대역으로 대체했다. 테스트 코드를 작성하다 보니 애매한 부분이 생겼다. 테스트하려는 코드는 대략 이런 느낌이었고, 다양한 상황을 세팅하고 getUsableCoupons를 호출했을 때 적절한 쿠폰이 반환되는지를 확인하려고 했다.
@Service
class CouponService(
// ...
private val couponRepository: CouponRepository
) {
fun getUsableCoupons(): List<Coupon> {
// ...
val usableCouponIds = getUsableCouponIds(targetCoupon)
return couponRepository.findAllByCouponIdsIn(usableCouponIds) // 여기를 Stubbing 해야 됨
}
private fun getUsableCouponIds(targetCouponIds: List<Long>): List<Long> {
// 모든 쿠폰 중 사용 가능한 쿠폰만 추리는 로직
}
}
- 먼저 해당 상품에 사용 가능한 대상이 되는 쿠폰을 모두 구한다.
- 쿠폰 중 현재 상품에 대해 사용 가능한 쿠폰번호만 추출한다.
- 추출한 쿠폰번호를 바탕으로 Repository에서 사용 가능한 쿠폰을 조회한다.
문제는 여기서 2번과 3번이었다. 그 이유는 쿠폰번호를 추출하는 로직이 private 메서드로 내부 구현으로 감추어져 있었다. 그걸 Stubbing하자니 테스트 코드 상에서 구현이 너무 깊게 노출되는 문제가 발생한다. 물론 Mock 객체를 쓰는 순간 구현이 노출되는 것은 어쩔 수 없지만, 개인적으로 가능한 한 줄이는 게 가독성이나 유지보수 측면에서 좋다고 생각한다.
아래 코드와 같은 방식으로 Stubbing을 하게 될 텐데, input으로 어떤 값이 들어갈 지 자체를 정의해주는 게 굉장히 껄끄러운 상황이었다. 사실 해당 Repository 메서드의 실제 기능은 매우 단순하다. 그냥 입력으로 주어진 쿠폰 번호를 terms 쿼리로 Elastic Search에 날리는 것이다. 즉, 주어진 쿠폰 번호에 대한 쿠폰만 반환해주는 기능이다.
// Input으로 들어가는 쿠폰번호 list가 private 메서드에 의해 추출되는 상황
// Output은 단순히 Input으로 주어지는 쿠폰번호에 해당하는 쿠폰 list
every { couponRepository.findAllByCouponIdsIn(listOf(1L, 2L, 3L, ...)) } returns
listOf(
CouponFixture(couponId = 1L),
CouponFixture(couponId = 2L),
CouponFixture(couponId = 3L),
)
어떻게 할 지 고민하던 도중 Stub 객체를 직접 만들고, 재사용하는 방법을 떠올렸다. 기존의 Repository의 메서드 인터페이스를 그대로 옮겨 인터페이스를 선언하고, Repository가 이것을 구현하게 한다.
// AS-IS
@Repository
class CouponRepository(
// ...
) {
fun findAllByCouponIdsIn(couponIds: List<Long>): List<Coupon> {
//...
}
}
// TO-BE
interface CouponRepository {
fun findAllByCouponIdsIn(couponIds: List<Long>): List<Coupon>
}
@Repository
class CouponRepositoryImpl : CouponRepository (
// ...
) {
override fun findAllByCouponIdsIn(couponIds: List<Long>): List<Coupon> {
//...
}
}
그리고 테스트 패키지 내에 Stub Repository를 선언하고, 마찬가지로 Stub Repository가 이를 구현하게 한다. 여기선 픽스쳐를 사용했지만, Map을 활용해 동적으로 객체를 넣어주는 방식도 가능하다(Fake).
class StubCouponRepository: CouponRepository {
override fun findAllByCouponIdsIn(couponIds: List<Long>): List<Coupon> {
return couponFixtures.filter { couponIds.contains(it.couponId) }
}
}
테스트 클래스 내에서 Stub 객체를 생성해서 주입해주면 별도의 추가적인 Stubbing 없이 테스트가 가능해진다.
// AS-IS
CouponServiceTest {
private val couponRepository: CouponRepository = mockk()
//...
}
// TO-BE
CouponServiceTest {
private val couponRepository: CouponRepository = StubCouponRepository()
//...
}
'테스트코드' 카테고리의 다른 글
[ATDD] 인수테스트 주도 개발에 대한 짧은 고찰 (0) | 2024.05.05 |
---|