보통 네트워크 통신 테스트를 위해 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
'안드로이드 > 개발관련(Kotlin)' 카테고리의 다른 글
| [Android] gradle 관리를 위한 build-logic 패턴 (0) | 2025.11.30 |
|---|---|
| [Android] 라이브러리 없이 직렬화/역직렬화 구현 (0) | 2025.11.30 |
| [Android] HttpsURLCoonection을 사용한 인터넷 통신 (0) | 2025.11.30 |
| [Android] jvmTarget 지정 방식이 deprecated 된 이유 (0) | 2025.11.29 |
| [Android] TYPESAFE_PROJECT_ACCESSORS (1) | 2024.10.09 |