Yebali

[Spring] 테스트에서 Static 메소드 Mocking하기 본문

Spring

[Spring] 테스트에서 Static 메소드 Mocking하기

예발이 2022. 9. 24. 01:47

회사에서 테스트 코드를 병렬 처리 설정을 넣고 Codebuild를 돌렸더니 아래처럼 에러가 발생했다.

심지어 매번 돌릴 때마다 결과가 달라져 동시성 문제인 것을 예상할 수 있었다.

 

 

문제의 테스트 코드

@ExtendWith(MockKExtension::class)
class TestServiceTest {
    
    ...

    @InjectMockKs
    private lateinit var service: TestService


    @BeforeEach
    fun initTest() {
        // for LocalDate.now() Mocking
        mockkStatic(LocalDate::class)

        ...
    }

    @Test
    fun Test_A() {
        
        ...

        every { LocalDate.now(ZoneOffset.ofHours(9)) } returns LocalDate.of(2022, 7, 11)

        ...

        val expectedDates = listOf(
            LocalDate.of(2022, 7, 11),
            LocalDate.of(2022, 7, 12),
            LocalDate.of(2022, 7, 13),
            LocalDate.of(2022, 7, 14)
        )

        assertThat(availableDates).containsExactlyElementsOf(expectedDates)
    }

    @Test
    fun Test_B() {
        
        ...

        every { LocalDate.now(ZoneOffset.ofHours(9)) } returns LocalDate.of(2022, 7, 30)
        
        ...

        // then
        val expectedDates = listOf(
            LocalDate.of(2022, 7, 30),
            LocalDate.of(2022, 7, 31),
        )

        assertThat(availableDates).containsExactlyElementsOf(expectedDates)
    }

    @Test
    fun Test_C() {
        
        ...

        every { LocalDate.now(ZoneOffset.ofHours(9)) } returns LocalDate.of(2022, 7, 11)
    
        ...

        val expectedDates = listOf(
            LocalDate.of(2022, 7, 11),
            LocalDate.of(2022, 7, 13),
            LocalDate.of(2022, 7, 14)
        )

        assertThat(availableDates).containsExactlyElementsOf(expectedDates)
    }

    @Test
    fun Test_D() {
        
        ...

        every { LocalDate.now(ZoneOffset.ofHours(9)) } returns LocalDate.of(2022, 7, 11)
        
        ...

        val expectedDates = listOf(
            LocalDate.of(2022, 7, 11),
            LocalDate.of(2022, 7, 14)
        )

        assertThat(availableDates).containsExactlyElementsOf(expectedDates)
    }
}

테스트 코드에서 `@BeforeEach`내부에 `mockkStatic()`을 사용하여 `LocaDate`를 mocking 하고,
각 테스트에서는 `LocalDate.now()` 매서드 값을 mocking 하고 있다.

 

다른 테스트들은 `LocalDate.now()`의 값을 `LocalDate.of(2022, 7, 11)`로 mocking 하지만, 
`Test_B`는 `LocalDate.of(2022, 7, 30)`으로 mocking한다. 여기서 문제는 `LocalDate.now()`는 static 메서드로 mocking하면 다른 테스트에 영향을 미친다.

public static LocalDate now(ZoneId zone) {
    return now(Clock.system(zone));
}

해결 방법

결론부터 말하면 `LocalDate.now()`를 `@Service` 내부에서 Static 메서드를 사용하지 말고 외부에서 만들도록 하는 것이다.

이를 테면 아래처럼 `LocalDate.now()`를 통해 현재 날짜를 생성하는 기능을 가진 @Bean을 주입받아 사용하는 것이다.

@Service
class TestService(
    ...
    private val localDateGenerator: LocalDateGenerator,
) {

    ...

    private fun List<LocalDate>.excludeDatesPastBoundary(window: Window): List<LocalDate> {
        val now = localDateGenerator.now(window.zoneOffset)
        
        ...

    }
}

위에 코드 처럼 `TestService`에 `LocalDateGenerator`를 주입받아 해당 서비스의 `.now()` 메서드를 사용해서 현재 날짜를 받도록 수정했다.

 

위의 수정으로 테스트 코드에서 현재 날짜를 mocking하는 부분을 아래처럼 수정했다.

every { localDateGenerator.now(any()) } returns LocalDate.of(2022, 7, 11)

위의 수정으로 동시성이슈는 해결했다.

 

하지만 테스트 코드를 위해 `@Service`코드를 수정하는 게 맞는 행위일까? 

결론부터 말하면 말하면 맞다.

토비의 스프링에서는 테스트할 대상이 의존하고 있는 Bean을 DI를 통해 바꿔치기 하는 것 을 서비스 추상화라고 하며,
테스트를 원할하게 하기 위해서만이라도 서비스 추상화를 하는 것은 바람직하다고 한다.

 

글 쓰는데 참조한 블로그 글에 자세한 내용이 있다.

참조

https://velog.io/@betterfuture4/Static-%EB%A9%94%EC%86%8C%EB%93%9C%EB%A5%BC-mocking%ED%95%98%EC%A7%80-%EB%A7%90%EC%9E%90-feat.-LocalDate.nowclock

'Spring' 카테고리의 다른 글

WebSocket 구현하기 feat.Spring  (0) 2023.02.10
Spring Collection 조회 성능 비교  (0) 2022.12.25
Spring의 Filter와 Interceptor  (0) 2022.01.23
Join의 종류와 QueryDsl  (0) 2021.11.01
QueryDSL in Kotlin  (0) 2021.10.31