본문 바로가기
개발 이야기/Springboot

[Kotlin] Springboot, Mock을 활용한 유닛 테스트(feat. Method Stub)

by 농개 2021. 2. 27.
반응형

Springboot 기반의 애플리케이션을 개발할 때, 주로 JUnit으로 테스트 코드를 작성하게 될겁니다.

(Springboot 2.2 버전 이후 부터는 JUnit 4가 아닌 5버전을 디폴트로 사용하게 됩니다.)

자바 개발자의 대다수가 JUnit을 사용해 테스트를 하기 때문에 기본적인 사용법에 대해선 너무나 잘 알려져 있기도 합니다.

JUnit은 다음과 같은 Annotation을 제공합니다.

  • @SpringBootTest : Spring이 기본적으로 사용하는 의존성 추가 해줌( + JUnit 5에서는 추가해주지 않아도 된다고하네요.)
  • @Test : 테스트 메서드 수행
  • @BeforeAll : 각 테스트 메서드들 수행 전 실행
  • @Mock : 더미객체를 만듬
  • etc...

그리고 Assertion을 통해서 작성한 메서드의 다양한 결과값을 검증해 볼수 있습니다.

  • assertEquals(expected, actual) : 기대한값과 코드 실행 결과가 같은지 확인
  • assertNotNull(actual) : 실행결과가 null이 아닌지 확인
  • etc...

 

해당 글에서는 실제 현업에서 많이 쓰이는 외부 HTTP 요청 코드가 포함된 서비스를 테스트하는 방법을 예제를 통해 알아봅니다.

이때 Mockito가 제공하는 Mock과 Stubbing 을 사용하면, 코드의 외부 요청 부를 대리하는 코드를 만들어 낼 수 있습니다.

더보기

Mock : 진짜 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체 (인프런 " 더 자바, 코드를 테스트하는 다양한 방법" 에서 발췌.)

Stubbing메소드 스텁 혹은 간단히 스텁은 소프트웨어 개발에 쓰이고 다른 프로그래밍 기능을 대리하는 코드이다. 스텁은 기존 코드를 흉내내거나 아직 개발되지 않은 코드를 임시로 대치하는 역할을 수행한다.(위키백과 발췌)

 

 

01. RestTemplate 컴포넌트 코드 작성

@Configuration
class RestTemplateConfig {

    @Bean
    fun restTemplate(): RestTemplate{
        val factory = HttpComponentsClientHttpRequestFactory()
        val client = HttpClientBuilder.create()
            .setMaxConnTotal(50)
            .setMaxConnPerRoute(20)
            .build()

        factory.httpClient = client
        factory.setConnectTimeout(3000)
        factory.setReadTimeout(5000)

        return RestTemplate(factory)
    }
}

 

HTTP통신으로 외부자원을 요청하기 위해 RestTemplate 을 사용할 예정이기에 위와 같이 Bean으로 등록하는 코드를 작성해줍니다.

 

 

 

02.  Service 코드 작성

@Service
class ExtRequestServiceImpl(
    val restTemplate: RestTemplate   // 의존성 추가
): ExtRequestService {
    private inline fun <reified T: Any> typeRef(): ParameterizedTypeReference<T> = object: ParameterizedTypeReference<T>(){}

    override fun getExtResource(): Any {
        val response = restTemplate.exchange(
            "https://external-domain/api/getSomething", HttpMethod.GET, null, typeRef<Any>()
        )
        // 위 요청 결과에 따른 비즈니스 로직 수행...

        return mapOf(
            "result" to "SUCCESS"
        )
    }
 }

대략 위와 같이 서비스 메서드를 작성해줬습니다.

 

위 코드는 https://external-domain/api/getSomething 경로로 http요청을 날리는 코드가 포함되어, 외부 의존성이 생기게 됩니다.

이러한 유형의 메서드는 테스트 코드를 작성함에 있어 다양한 사이드 이펙트가 생길수 있습니다. 접근이 제한되거나 외부 서비스의 개발이 진행중이거나.. 기타 등등의 형태로 말입니다.

즉, 함수를 테스트하는데 있어 멱등성을 해칩니다.

이제 restTemplate.exchange(...) 부분을 항상 같은 값을 반환하도록 하여 정상적인 유닛 테스트가 수행되도록 해봅시다.

 

 

 

03. Test 코드 작성

Mockito를 활용하여 RestTemplateexchain 함수를 stubbing 하는 테스트 코드를 작성해봅시다.

@SpringBootTest
class ExtRequestServiceImplTest{

    @Autowired
    private lateinit var extRequestService: ExtRequestService
    @MockBean
    private lateinit var restTemplate: RestTemplate
    
    private inline fun <reified T: Any> typeRef(): ParameterizedTypeReference<T> = object: ParameterizedTypeReference<T>(){}

    @Test
    fun getExtResourceTest(){
        `when`(restTemplate.exchange(
            "https://external-domain/api/getSomething", HttpMethod.GET, null, typeRef<Any>()
        )).thenReturn(ResponseEntity.ok("mock test"))

        val response = extRequestService.getExtResource()

        assertNotNull(response)
        assertEquals(response, mapOf("result" to "SUCCESS")
    }
}
  • @Autowired private lateinit ~을 통해 앞서 만든 extRequestService에 대한 의존성을 추가해줍니다
  • @MockBean으로 restTemplate의 경우 Mock 객체화 시켜줍니다.
    • restTemplatecontainer에 등록될 bean이며, extRequestService 내에서도 의존성을 주입 받아 사용하는 형태 입니다. 즉, Spring Application Context 레벨에서 관리되고 생성됩니다.
    • @Mock의 경우 SpringRunner로 실행되는 테스트코드에서 정상적으로 Mocking 되지 않습니다. @ExtendWith(MockitoExtenstion.class.java) 를 추가하면 되겠지만, SpringRunner에 실행되는 테스트코드에서 클래스를 Mocking 하려면 @MockBean을 사용해주면 @ExtendWith를 사용할 필요가 없습니다.
  • when().thenReturn() 으로 특정 행위에 대해서 미리 정의한 결과값을 받도록 Stubbing 합니다.
    • restTemplate bean 객체의 exchange 메서드의 행위를 미리 정의합니다. 
    • 테스트 코드는 SpringRunner로 실행되며, 이후 실행될 restTempalte.exchange(~~)는 무조건 ResponseEntity.ok("mock test")를 반환하게 됩니다.

근데 위와 같은 코드의 경우 exchange 메서드의 파라미터를 직접 지정해줬습니다. 

Mockito가 제공하는 일종의 matcher를 사용하면 직접 파라미터를 관리해줄 필요가 없게됩니다.

 

 

 

04. ArgumentMatcher

아래와 같이 when().thenReturn() 부분을 변경해봅시다.

`when`(restTemplate.exchange(
	Mockito.anyString(),
	Mockito.any(HttpMethod::class.java),
	Mockito.eq(null),
	Mockito.eq(typeRef<Any>())
)).thenReturn(ResponseEntity.ok("mock test"))

아래와 같이 primitive 한 값들을 대체 해서 표시할 수 있습니다.

  • Mockito.anyString()
  • Mockito.anyInt()
  • Mockito.anyLong()
  • ...

any(Class<T> type)의 형태로 Object 타입도 매쳐를 사용할 수 있습니다.

  • Mockito.any(Member::class.java)

eq()를 통해서 값을 지정 할 수도 있구요.

  • Mockito.eq(null)
  • Mockito.eq(10)
  • ...

 

반응형