240708 실전 프로젝트 - 테스트 코드 세션, Spring context의 테스트 인스턴스화와 Bean 주입

노재원·2024년 7월 8일
0

내일배움캠프

목록 보기
77/90
post-custom-banner

테스트 코드 세션

지난 번에도 테스트 코드 세션을 진행했었지만 이번엔 다시 다른 튜터님에게 테스트 코드에 대한 설명을 들었다. 시나리오나 유의할 점등은 공통적인 부분이 많았지만 다른 습관에 대해서도 공유가 됐다.

현재 진행하고 있는 동시성 제어 프로젝트에서도 티켓 발급에 대한 테스트 코드를 먼저 작성해두니 Lock, Transactional의 구조 변경이 이루어져도 테스트가 쉬워 적용이 좋았다.

초기 작업 소요시간은 더 투자했지만 현재는 공부를 진행하며 변경을 하면서도 높은 생산성을 지닐 수 있게 됐다.

외부 시스템의 테스트

직접 외부 시스템을 그대로 의존하는 것은 FIRST 원칙중 Isolated, Repeatable 을 위배할 수 있어 반드시 Mock Server 를 활용해 단위 테스트 하는 것을 추천해주셨고 그에 대한 예제를 보여주셔서 참고가 잘 됐다.

외부 시스템의 응답 방식 자체가 바뀔 일도 자주 있진 않으니 미리 한 번의 데이터를 작성하면 좋은 테스트가 된다.

class KakaoOAuth2ClientTest {

    private lateinit var server: MockWebServer
    private val restClient = RestClient.create()

    @BeforeEach
    fun setUp() {
        server = MockWebServer()
    }

    @Test
    fun `getAccessToken - 정상조회시 동작 확인`() {
        // GIVEN
        val client = KakaoOAuth2Client(
            clientId = "CLIENT_ID",
            redirectUrl = "http://localhost:8080/oauth2/callback/kakao",
            authServerBaseUrl = server.url("/").toString(),
            resourceServerBaseUrl = server.url("/").toString(),
            restClient = restClient
        )
        MockResponse()
            .addHeader("Content-Type", MediaType.APPLICATION_JSON)
            .setResponseCode(200)
            .setBody(
                """
                    {
                        "access_token":"bbJ7t-hf8eJZSgflHk0SbgFkJIT6mx7IqKgKKiWPAAABjP0hJCUp9hBbJybEWQ",
                        "token_type":"bearer",
                        "refresh_token":"wiZbj8VafhZYxk0tdqt3MEPngHqdvT9nHCwKKiWPAAABjP0hJCIp9hBbJybEWQ",
                        "expires_in":21599,
                        "scope":"profile_nickname",
                        "refresh_token_expires_in":5183999
                    }
                """.trimIndent()
            )
            .let { server.enqueue(it) }

        // WHEN
        val accessToken = client.getAccessToken("TEST CODE")

        // THEN
        accessToken shouldBe "bbJ7t-hf8eJZSgflHk0SbgFkJIT6mx7IqKgKKiWPAAABjP0hJCUp9hBbJybEWQ"
    }

    @Test
    fun `getAccessToken - 에러발생시 동작 확인`() {
        // GIVEN
        val client = KakaoOAuth2Client(
            clientId = "CLIENT_ID",
            redirectUrl = "http://localhost:8080/oauth2/callback/kakao",
            authServerBaseUrl = server.url("/").toString(),
            resourceServerBaseUrl = server.url("/").toString(),
            restClient = restClient
        )
        MockResponse()
            .addHeader("Content-Type", MediaType.APPLICATION_JSON)
            .setResponseCode(500)
            .let { server.enqueue(it) }

        // WHEN & THEN
        shouldThrow<RuntimeException> {
            client.getAccessToken("TEST_CODE")
        }.let {
            it.message shouldBe "카카오 AccessToken 조회 실패"
        }
    }
}

JSON String 이 짧다면 테스트 코드내에 포함시켜도 되지만, 길어진다면 ../resource 밑에 .json 형태로 두고 파일을 읽어서 테스트 코드에서 활용할 수 있다.

트러블 슈팅 - Spring context의 테스트 인스턴스 생성과 빈 주입 (생명주기)

// 에러가 발생한 부분
@SpringBootTest
class LockServiceTest(
    
) : BehaviorSpec({
    val lockService = LockService(redisLockRepository)
    })
    
// kotlin.UninitializedPropertyAccessException: lateinit property advice has not been initialized

이를 변경해서 실행이 된 코드는 다음과 같다.

@SpringBootTest
class LockServiceTest(
    private val redisLockRepository: RedisLockRepository // 사용하지 않음, 아무상관없는 Bean을 주입해도 동작함
) : BehaviorSpec({
    val lockService = LockService(redisLockRepository)
      
// 또는 BehaviorSpec을 사용하지 않고 @Test 로 작성시 문제가 없다.

@SpringBootTest
class LockServiceTest {
		@Test
    fun test() {
        /*...*/    
    }
}

동일한 테스트 코드를 BehaviorSpec이 아닌 @Test를 사용시 이러한 문제가 발생하지 않는 것으로 보아 BehaviorSpec 생성자의 Bean과 @Test의 Bean 주입이 차이를 보인 것으로 보인다.

Spring context가 테스트 인스턴스를 만들고 Bean을 주입하는 과정

Spring Context가 테스트 인스턴스의 생성을 관리할 때 생성자가 존재하면 생성자에 대한 Component 스캔이 이루어지고 적절한 Bean을 찾아 주입하지만 생성자가 비어있을 경우 빈 생성자로 인스턴스가 생성되고 관리된다. 이 과정에서 생성자에 대한 ComponentScan 과정이 발생하기는 하지만 생성자가 비어있어 사실상 Bean의 주입이 처리되지 않는다.

@Test로 작성한 코드는 독립적으로 실행되어 생성자 주입 단계를 넘어서 다시 메소드로 넘어온 단계이기 때문에 다시 Bean 탐색 과정을 거치고 주입한다.

하지만 BehaviorSpec의 경우 생성자로 즉시 주입되는 방식이므로 이를 방지하기 위한 방법으로 SpringTestLifecycleMode.ROOT 를 설정해 Spring context가 테스트 전후로 다시 Setup되게 설정하여 방지할 수도 있다.

@SpringBootTest
class LockServiceTest : BehaviorSpec({
    extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) // <- 이거
}

기본 설정된 SpringTestLifecycleModePER_METHOD 인데 이는 각 테스트 메서드가 실행될 때마다 새로운 테스트 인스턴스의 생성자에 관여한다. 생성자의 호출에 따른 Bean의 주입인 건 여전하므로 이 때 주입이 이루어지지 않는 것으로 추측할 수 있다.

ROOT 의 경우 인스턴스의 생성과 Bean 주입의 경계가 명확해진다. 생성자에 쓰이지 않았던 의존성 또한 경계가 생겨 독립적으로 처리하기 때문에 생성자와 관계 없던 Bean의 의존성 주입까지 명확하게 처리된다고 추측할 수 있다.

100% 정확한 답인지 확신을 가지긴 너무 깊이 Context를 조사하며 어려움을 겪었지만 생성자 주입 방식에서 발생할 수 있는 이슈라 느껴졌다.

post-custom-banner

0개의 댓글