안드로이드/개발관련(Kotlin)

[Android] 라이브러리 없이 클라이언트에서 테스트 서버 구현

닉네임못짓는사람 2025. 11. 30. 02:52
반응형

보통 네트워크 통신 테스트를 위해 OkHttp의 MockWebServer를 많이 사용하는데,

해당 라이브러리 없이 테스트 서버를 구현하고 실제 테스트 코드를 작성해보도록 하겠다.

 

private var httpsServer: HttpsServer? = null
private var httpServer: HttpServer? = null
fun start() {
    httpsServer = HttpsServer.create(InetSocketAddress(MOCK_PORT_HTTPS), 0).apply {
        httpsConfigurator = getTestHttpsConfigurator()
        createContexts()
        executor = null
        start()
    }

    httpServer = HttpServer.create(InetSocketAddress(MOCK_PORT_HTTP), 0).apply {
        createContexts()
        executor = null
        start()
    }
}

구현에는 HttpsServer와 HttpServer를 사용하는데, 두 요청을 모두 처리하기 위해 둘 다 선언하였고,

서버 시작시에 둘을 초기화한다.

 

private fun getTestHttpsConfigurator(): HttpsConfigurator {
    val keyStore = KeyStore.getInstance("JKS")
    File("test_keystore.jks").inputStream().use {
        keyStore.load(it, "password".toCharArray())
    }

    val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
    kmf.init(keyStore, "password".toCharArray())

    val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
    tmf.init(keyStore)

    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(kmf.keyManagers, tmf.trustManagers, java.security.SecureRandom())

    return HttpsConfigurator(sslContext)
}

이떄 HttpsServer의 경우 인증서를 설정해줘야하는데, 테스트 서버이기 때문에 테스트용 인증서로 대체하여 사용한다.

 

private fun HttpServer.createContexts() {
    createContext(PATH_SUCCESS, SuccessHandler())
    createContext(PATH_CLIENT_ERROR, ClientErrorHandler())
    createContext(PATH_SERVER_ERROR, ServerErrorHandler())
    createContext(PATH_SUCCESS_WITH_REQUEST, RequestWithBodyHandler())
    createContext(PATH_HEADER, HeaderHandler())
    createContext(PATH_TIMEOUT, TimeoutHandler())
}

createContext는 서버에서 특정 URL에 대한 요청 경로를 지정하는 함수이며, HttpHandler를 parameter로 가진다.

현재 추가한 경로는 200, 400, 500, request추가, 헤더 추가, 타임아웃 테스트를 위한 경로이다.

 

fun sendCommonResponse(exchange: HttpExchange, resultCode: Int, response: String) {
    exchange.responseHeaders.add("Content-Type", "application/json")
    exchange.sendResponseHeaders(resultCode, response.toByteArray().size.toLong())
    exchange.responseBody.use { os: OutputStream ->
        os.write(response.toByteArray())
    }
}

private inner class SuccessHandler : HttpHandler {
    override fun handle(exchange: HttpExchange) {
        sendCommonResponse(exchange, 200, TEST_DATA_JSON_SUCCESS)
    }
}

200테스트시에 호출할 HttpHandler의 예시

 

테스트에선 JUnit4를 사용한다.

class MyHttpClientTest {
    private val mockWebServer = MockWebServer()
    private val converter = mockWebServer.converter
    private lateinit var client: HttpClient

	// 테스트 서버 인증서는 정식으로 인증받은게 아니기 때문에 모든 인증서를 허가하기 위해 추가
    val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
        override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
        override fun checkClientTrusted(certs: Array<java.security.cert.X509Certificate>, authType: String) {}
        override fun checkServerTrusted(certs: Array<java.security.cert.X509Certificate>, authType: String) {}
    })

    private val httpsServerUrl = "https://localhost:$MOCK_PORT_HTTPS"
    private val httpServerUrl = "http://localhost:$MOCK_PORT_HTTP"

    private lateinit var mockSuccess: TestResponse
    private lateinit var mockClientError: TestResponse
    private lateinit var mockServerError: TestResponse

    @Before
    fun setup() {
        mockSuccess = converter.deserialize(MockWebServer.TEST_DATA_JSON_SUCCESS, TestResponse::class)
        mockClientError = converter.deserialize(MockWebServer.TEST_DATA_JSON_CLIENT_ERROR, TestResponse::class)
        mockServerError = converter.deserialize(MockWebServer.TEST_DATA_JSON_SERVER_ERROR, TestResponse::class)

        setClient(httpsServerUrl)
        mockWebServer.start()
    }

    private fun setClient(baseUrl: String) {
        client = getClientBuild(baseUrl)
            .build()
    }

	//	테스트마다 다른 client가 필요할 수 있기 때문에 별도 함수로 분리
    private fun getClientBuild(baseUrl: String) = MyHttpClient.Builder()
        .setBaseUrl(baseUrl)
        .setConverter(converter)
        .setConnectionFactory {
            val connection = URL(it).openConnection()
            if (it.contains("https://")) {
                (connection as HttpsURLConnection).apply {
                    hostnameVerifier = HostnameVerifier { _, _ -> true}
                    trustAllCerts()
                }
            } else {
                connection as HttpURLConnection
            }
        }

    private fun HttpsURLConnection.trustAllCerts() {
        val sslContext = SSLContext.getInstance("TLS")
        sslContext.init(null, trustAllCerts, java.security.SecureRandom())
        this.sslSocketFactory = sslContext.socketFactory
    }

    @After
    fun clean() {
        mockWebServer.stop()
    }

    @Test
    fun `HTTP URL에 GET 요청을 보내고 올바른 응답을 받는다`() {
        setClient(httpServerUrl)

        val res: TestResponse = client.doRequest(
            RequestMethod.GET,
            MockWebServer.PATH_SUCCESS,
            TestResponse::class
        )
        assert(res == mockSuccess)
    }

    @Test
    fun `HTTPS URL에 GET 요청을 보내고 올바른 응답을 받는다`() {
        val res: TestResponse = client.doRequest(
            RequestMethod.GET,
            MockWebServer.PATH_SUCCESS,
            TestResponse::class
        )
        assert(res == mockSuccess)
    }
}

 

 

MockWebServer전체 코드

class MockWebServer {
    companion object {
        const val MOCK_PORT_HTTPS = 8080
        const val MOCK_PORT_HTTP = 8081
        const val PATH_SUCCESS = "/success"
        const val PATH_CLIENT_ERROR = "/clienterr"
        const val PATH_SERVER_ERROR = "/servererr"
        const val PATH_SUCCESS_WITH_REQUEST = "/successrequest"
        const val PATH_HEADER = "/header"
        const val PATH_TIMEOUT = "/timeout"

        const val TEST_DATA_JSON_SUCCESS = "{\"code\":200,\"message\":\"success\"}"
        const val TEST_DATA_JSON_CLIENT_ERROR = "{\"code\":400,\"message\":\"client error\"}"
        const val TEST_DATA_JSON_SERVER_ERROR = "{\"code\":500,\"message\":\"server error\"}"
    }

    private var httpsServer: HttpsServer? = null
    private var httpServer: HttpServer? = null
    internal val converter = MyJsonConverter()

    fun start() {
        httpsServer = HttpsServer.create(InetSocketAddress(MOCK_PORT_HTTPS), 0).apply {
            httpsConfigurator = getTestHttpsConfigurator()
            createContexts()
            executor = null
            start()
        }

        httpServer = HttpServer.create(InetSocketAddress(MOCK_PORT_HTTP), 0).apply {
            createContexts()
            executor = null
            start()
        }
    }

    fun stop() {
        httpsServer?.stop(0)
        httpServer?.stop(0)
    }

    private fun getTestHttpsConfigurator(): HttpsConfigurator {
        val keyStore = KeyStore.getInstance("JKS")
        File("test_keystore.jks").inputStream().use {
            keyStore.load(it, "password".toCharArray())
        }

        val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
        kmf.init(keyStore, "password".toCharArray())

        val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
        tmf.init(keyStore)

        val sslContext = SSLContext.getInstance("TLS")
        sslContext.init(kmf.keyManagers, tmf.trustManagers, java.security.SecureRandom())

        return HttpsConfigurator(sslContext)
    }

    private fun HttpServer.createContexts() {
        createContext(PATH_SUCCESS, SuccessHandler())
        createContext(PATH_CLIENT_ERROR, ClientErrorHandler())
        createContext(PATH_SERVER_ERROR, ServerErrorHandler())
        createContext(PATH_SUCCESS_WITH_REQUEST, RequestWithBodyHandler())
        createContext(PATH_HEADER, HeaderHandler())
        createContext(PATH_TIMEOUT, TimeoutHandler())
    }

    fun sendCommonResponse(exchange: HttpExchange, resultCode: Int, response: String) {
        exchange.responseHeaders.add("Content-Type", "application/json")
        exchange.sendResponseHeaders(resultCode, response.toByteArray().size.toLong())
        exchange.responseBody.use { os: OutputStream ->
            os.write(response.toByteArray())
        }
    }

    private inner class SuccessHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            sendCommonResponse(exchange, 200, TEST_DATA_JSON_SUCCESS)
        }
    }

    private inner class ClientErrorHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            sendCommonResponse(exchange, 400, TEST_DATA_JSON_CLIENT_ERROR)
        }
    }

    private inner class ServerErrorHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            sendCommonResponse(exchange, 500, TEST_DATA_JSON_SERVER_ERROR)
        }
    }

    private inner class RequestWithBodyHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            val requestBody = exchange.requestBody.bufferedReader().use { it.readText() }
            val response = "{\"code\":200,\"message\":\"success\",\"requestBody\":$requestBody}"
            sendCommonResponse(exchange, 200, response)
        }
    }

    private inner class HeaderHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            val headers = exchange.requestHeaders
                .map { Header(it.key, it.value.firstOrNull() ?: "") }
                .filter { it.key.isNotBlank() && it.value.isNotBlank() }
            val response = TestResponse(200, "success", headers = headers)
            val json = converter.serialize(response)
            sendCommonResponse(exchange, 200, json)
        }
    }

    private inner class TimeoutHandler : HttpHandler {
        override fun handle(exchange: HttpExchange) {
            Thread.sleep(100)
            sendCommonResponse(exchange, 200, TEST_DATA_JSON_SUCCESS)
        }
    }
}

 

테스트하는 HttpClient 코드는 아래 링크에서 확인

https://angangmoddi.tistory.com/339

 

[Android] HttpsURLCoonection을 사용한 인터넷 통신

interface HttpClient { companion object { const val DEFAULT_CONNECTION_TIMEOUT = 15_000 const val DEFAULT_READ_TIMEOUT = 15_000 } fun doRequest( method: RequestMethod, path: String, outClass: KClass, headers: List = emptyList(), body: IN? = null ): OUT fun

angangmoddi.tistory.com

 

반응형