모종닷컴

테스트 코드에서 클래스 생성에 필요한 모든 클래스를 mocking 본문

Programming/Spring

테스트 코드에서 클래스 생성에 필요한 모든 클래스를 mocking

모종 2022. 10. 10. 20:48
반응형

진행하고 있는 프로젝트가 슬슬 마무리가 되어가네요.. 포스팅할 거리가 많은데 프로젝트가 끝나면 하나씩 정리해서 올리도록 하겠습니다. 너무 오랫동안 글을 못 올리는 게 싫어서 짧은 글이지만 소개해보려고 합니다.

의존하고 있는 빈이 많은 클래스들이 간혹 보입니다. 설계를 잘못한걸수도 있지만 이런 클래스들은 특히 단위 테스트 코드를 짤 때 정말 귀찮습니다. 예시를 보도록 하겠습니다.

예시 코드

private val log = KotlinLogging.logger {  }

@Service
class WelcomeStep {
    fun execute() {
        log.info("welcome")
    }
}

@Service
class EnjoyStep {
    fun execute() {
        log.info("enjoy")
    }
}

@Service
class GoodByeStep {
    fun execute() {
        log.info("good byte")
    }
}

(인터페이스로 묶고 싶은 욕망은 잠시 넣어두세요)

@Service
class GameService(
    private val welcomeStep: WelcomeStep,
    private val enjoyStep: EnjoyStep,
    private val goodByeStep: GoodByeStep
) {
    fun process() {
        welcomeStep.execute()
        enjoyStep.execute()
        goodByeStep.execute()
    }
}

 

테스트 코드

@RunWith(SpringJUnit4ClassRunner::class)
internal class GameServiceV1Test {
    @InjectMocks
    private lateinit var gameService: GameService
    private val welcomeStep: WelcomeStep = spy { }
    private val enjoyStep: EnjoyStep = spy { }
    private val goodByeStep: GoodByeStep = spy { }

    @Test
    fun test() {
        gameService.process()
    }
}

mockito-kotlin 라이브러리를 사용하였습니다.

testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'

위와 같이 테스트 코드를 짜볼수 있습니다. 여기서 저는 각각의 필요한 빈들을 매번 저런 식으로 넣어주어야 하는 게 귀찮았습니다.

예를 들어 GameService에 RestStep 이 추가된다 가정한다면 추후에 개발 중간 중간 테스트 코드가 아래의 예시와 같이 깨지는 것도 볼 수 있습니다.

restStep 추가

@Service
class GameService(
    private val welcomeStep: WelcomeStep,
    private val enjoyStep: EnjoyStep,
    private val goodByeStep: GoodByeStep,
    private val restStep: RestStep
) {
    fun process() {
        welcomeStep.execute()
        enjoyStep.execute()
        restStep.execute()
        goodByeStep.execute()
    }
}

Test Fail

Parameter specified as non-null is null: method GameService.<init>, parameter restStep

테스트코드 필드에 아래를 추가해주면 테스트가 통과됩니다.

private val restStep: RestStep = spy { }

이렇게 매 의존 클래스가 늘어갈 때마다 테스트 코드가 계속 수정되어야 합니다. 그래서 이런 귀찮음을 줄이고자 생성에 필요한 모든 클래스를 mock으로 만들어 주입해버리면 어떨까 싶었습니다.

생성자에 필요한 모든 클래스를 mock으로 만들어 생성

internal class GameServiceTest {
    private lateinit var gameService: GameService
    @Before
    fun getRequiredBean() {
        val constructor = GameService::class.java.constructors[0]

        // 생성에 필요한 클래스를 모두 mock으로 만들어 주입.
        val mockBeans = constructor.parameterTypes.map { Mockito.spy(it) }
        gameService = constructor.newInstance(*mockBeans.toTypedArray()) as GameService
    }

    @Test
    fun test() {
        gameService.process()
    }
}

이렇게 하면 클래스가 매번 늘어나더라도 코드의 수정이 필요하지 않습니다. 그리고 주입된 클래스가 필요한 경우에는 아래와 같은 방식으로 가져오면 됩니다.

@Test
fun test() {
    val restStep = ReflectionTestUtils.getField(gameService, "restStep") as RestStep
    whenever(restStep.execute()).thenAnswer { println("mock rest time..") }
    gameService.process()
}

 

반응형