2021.10.20-개발일지

spring에서 외부에서 제공하는 api를 테스트 하고 싶었습니다. webclient를 이용하는데 service코드를 직접 호출하여 결과를 확인 할 수도 있었지만, 호출 전 또는 외부 서버의 상황에 구애받지 않고 테스트 하고 싶었습니다. 공식 문서와 구글링 결과 많이 사용하는 프레임워크로는 mockserver-netty 또는 okhttp-mock등이 존재합니다.

개발환경

2가지 방식의 예제를 살펴보기전에 외부 api서버를 호출하는 service코드 및 http response를 정의합니다.

Person.kt
data class Person(
        val age: Int,
        val name: String,
)

예제에 사용될 model 입니다.

PersonApi.kt
@Component
class PersonApi(
        @Value("\${person.api.host}") val host: String,
        @Value("\${person.api.path}") val path: String,
) {
    private val webClient: WebClient = WebClient.builder()
            .build()

    fun getPersonById(): Person {
        return webClient
                .get()
                .uri(host + path)
                .retrieve()
                .bodyToMono(Person::class.java)
                .block()!!
    }
}

webclient를 이용하여 api를 호출합니다. 이때 baseUrl 과 path를 분리합니다.

GET /person Http 200 

{"name": "hahava","age": 30}

기대하는 결과값은 위와 같은 형태입니다.

OkHttp MockerServer

spring-docs 공식 문서에서 추천하는 프레임워크입니다. 안드로이드에서 okhttp를 webclient로 많이 사용하기 때문에 mockserver 개발시 함께 많이 사용하는 것으로 보입니다.

testImplementation("com.squareup.okhttp3:okhttp:4.9.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.2")

위와 같이 2개의 라이브러리를 추가합니다. 이때 두 라이브러리의 버전은 일치해야 하며, 의존성 문제로 okhttp가 반드시 필요합니다. (버전이 일치 하지 않거나 okhttp추가 하지 않을 경우 NoClassDeffoundError 발생)

@SpringBootTest
class OkHttpServerTest {
    lateinit var mockWebServer: MockWebServer

    lateinit var personApi: PersonApi

    @Value("\${person.api.path}")
    lateinit var path: String

    @BeforeEach
    fun init() {
        val mockServerPort = 8888

        mockWebServer = MockWebServer()
        mockWebServer.start(mockServerPort) // ... (1)

        personApi = PersonApi("http://localhost:$mockServerPort", path) // ... (2)
    }

    @AfterEach
    fun destroy() {
        mockWebServer.shutdown()
    }

    @Test
    fun getPersonTest() {
        val mockResponse = MockResponse() // ... (3)
                .setResponseCode(200)
                .setBody("""
                        {
                          "name": "hahava",
                          "age": 30
                        }
                """.trimIndent()) 
                .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)

        mockWebServer.enqueue(mockResponse) // ... (4)

        val person = personApi.getPersonById()
        assertThat(person.age).isEqualTo(30)
        assertThat(person.name).isEqualTo("hahava")

        val recordedRequest = mockWebServer.takeRequest() // ... (5)
        assertThat(recordedRequest.path).isEqualTo(path)
        assertThat(recordedRequest.method).isEqualTo(HttpMethod.GET.toString())
    }
}
  • (1): mock 서버를 실행할 port를 정합니다.
  • (2): 실행된 mock 서버와 동일한 port 및 host를 지정해야 합니다.
  • (3): 외부 서버의 api 응답에 대한 예상 결과 값을 지정합니다.
  • (4): 정해진 응답(MockResponse)을 queue에 추가합니다. 여러개의 resoonse를 설정하면 request전송 순서 별 매칭됩니다.
  • (5): 실제 service코드가 실행된 후 기록하여 해당 변수에 할당합니다. 선언 순서별로 추가적인 기록을 할 수 있습니다.

이떄 주의할점은 path 값에 상관없이 host:port 만 동일하면 request가 캡쳐됩니다.

Netty Mockserver

이름이 netty라서 꼭 netty와 관련있나 싶지만 딱히 연관성은 잘 모르겠습니다.

testImplementation("org.mock-server:mockserver-netty:5.11.1")
testImplementation("org.mock-server:mockserver-client-java:5.11.1")

OkHttp 와 비슷하게 동일한 버전으로 2가지 라이브러리를 추가합니다.

@SpringBootTest
class NettyServerTest {
    lateinit var mockWebServer: ClientAndServer

    lateinit var personApi: PersonApi

    @Value("\${person.api.path}")
    lateinit var path: String

    @BeforeEach
    fun init() {
        val mockServerPort = 8888

        mockWebServer = ClientAndServer.startClientAndServer(mockServerPort)// ... (1)
        personApi = PersonApi("http://localhost:$mockServerPort", path) // ... (2)
    }

    @AfterEach
    fun destroy() {
        mockWebServer.stop()
    }

    @Test
    fun getPersonTest() {
        mockWebServer.`when`( // ... (3)
                HttpRequest.request()
                        .withMethod(HttpMethod.GET.toString())
        ).respond(
                HttpResponse.response()
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withBody("""
                        {
                          "name": "hahava",
                          "age": 30
                        }
                        """.trimIndent()
                        )
        )

        val person = personApi.getPersonById()
        Assertions.assertThat(person.age).isEqualTo(30)
        Assertions.assertThat(person.name).isEqualTo("hahava")
    }
}
  • (1): mock 서버를 실행할 port를 정합니다.
  • (2): 실행된 mock 서버와 동일한 port 및 host를 지정해야 합니다.
  • (3): request에 대한 response값을 설정합니다. 이때 okhttp와는 다르게 반드시 path가 일치해야 성공합니다.

개인적으로는 netty-mockserver가 가독성이 더 좋아서 선호합니다.

카테고리:

업데이트:

댓글남기기