[Android] MockWebServer

๐Ÿ’ก์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ๊ณผ์ •์—์„œ ํ•™์Šตํ•œ MockWebServer์— ๋Œ€ํ•˜์—ฌ ๊ธฐ๋กํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

MockWebServer

  • MockWebServer๋Š” ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๊ฐ€์งœ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋ฏธ๋ฆฌ ์ •์˜๋œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ…Œ์ŠคํŠธ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
  • ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” OkHttp์—์„œ ์ œ๊ณตํ•˜๋ฉฐ Retrofit, OkHttp ๋“ฑ์˜ ๋„คํŠธ์›Œํฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ•จ๊ป˜ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

https://github.com/square/okhttp/tree/master/mockwebserver

 

okhttp/mockwebserver at master · square/okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM. - square/okhttp

github.com

์ฃผ์š” ๊ธฐ๋Šฅ

  • ๊ฐ€์งœ ์„œ๋ฒ„ ํ™˜๊ฒฝ ์ œ๊ณต
    • ์‹ค์ œ ์„œ๋ฒ„ ์—†์ด๋„ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์— ๋Œ€ํ•ด ๋ฏธ๋ฆฌ ์ •์˜๋œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๊ฒ€์ฆ
    • ํ…Œ์ŠคํŠธ ์ค‘์— ๋ฐœ์ƒํ•œ ์š”์ฒญ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‹ค์–‘ํ•œ ์‘๋‹ต ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    • HTTP ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ์„ค์ •ํ•˜๊ฑฐ๋‚˜, ์‘๋‹ต ์ง€์—ฐ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” ๋“ฑ ๋‹ค์–‘ํ•œ ๋„คํŠธ์›Œํฌ ์ƒํ™ฉ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Gradle

  • MockWebServer๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
dependencies {
    testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3'
}

MockWebServer ๊ตฌํ˜„

  • ์šฐ์•„ํ•œํ…Œํฌ์ฝ”์Šค ํ™œ๋™ ์ค‘์— MockWebServer๋ฅผ ํ™œ์šฉํ•œ Service๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์ปดํฌ๋„ŒํŠธ Service๊ฐ€ ์•„๋‹Œ, CRUD๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ Service๋กœ ๋„ค์ด๋ฐ์„ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด Retrofit์˜ ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์œผ๋กœ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
interface ProductService {
    fun findAll(): List<Product>

    fun findProductById(productId: Long): Product?

    fun findPagingProducts(
        offset: Int,
        pagingSize: Int,
    ): List<Product>
}

์‘๋‹ต ์ •์˜

  • MockResponse๋ฅผ ํ™œ์šฉํ•ด์„œ ์‘๋‹ต ์ฝ”๋“œ์™€ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • setResponseCode(200)์œผ๋กœ HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ, setBody()๋กœ ์‘๋‹ต ๋ณธ๋ฌธ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
private fun openServer(
    server: MockWebServer,
    body: String,
) {
    server.enqueue(MockResponse().setBody(body).setResponseCode(200))
}

MockProductService

  • MockProductService๋Š” ProductService๋ฅผ MockWebServer๋กœ ๊ตฌํ˜„ํ•œ ๊ฐ€์ƒ ํ™˜๊ฒฝ์ž…๋‹ˆ๋‹ค.
  • Request๋ฅผ ์ƒ์„ฑํ•ด์„œ Response๋ฅผ ๋ฐ›๊ณ , Gson ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • findPagingProducts()๋Š” ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์œ„์น˜์™€ ์ด ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ๋ฅผ ๋ฐ›์•„์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • ProductDatabase์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ Retofit ๊ธฐ๋ฐ˜์œผ๋กœ ์š”์ฒญํ•˜๋Š” ์ž‘์—…์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
class MockProductService : ProductService {
    private val client = OkHttpClient()

    override fun findPagingProducts(
        offset: Int,
        pagingSize: Int,
    ): List<Product> {
        val pagingData =
            if (offset >= ProductDatabase.products.size) {
                emptyList()
            } else {
                ProductDatabase.products.drop(offset)
            }.take(pagingSize)
        val body = Gson().toJson(pagingData)
        val serverRequest = makeServerRequest(body, "?offset=$offset&pagingSize=$pagingSize")
        val response: Response = client.newCall(serverRequest).execute()
        val responseBody = response.body?.string() ?: return emptyList()
        val productType = object : TypeToken<List<Product>>() {}.type
        return Gson().fromJson(responseBody, productType)
    }

    private fun makeServerRequest(
        body: String,
        requestUrl: String,
    ): Request {
        val server = MockWebServer()
        openServer(server, body)
        return Request.Builder()
            .url(makeServerUrl(server, requestUrl))
            .build()
    }

    private fun openServer(
        server: MockWebServer,
        body: String,
    ) {
        server.enqueue(MockResponse().setBody(body).setResponseCode(200))
    }

    private fun makeServerUrl(
        server: MockWebServer,
        requestUrl: String,
    ): String {
        val serverUrl = server.url(MOCK_SERVER_PATH).toString()
        return "${serverUrl}$requestUrl"
    }

    private fun makeResponse(serverRequest: Request): Response {
        return client.newCall(serverRequest).execute()
    }

    companion object {
        private const val DEFAULT_URL = ""
        private const val MOCK_SERVER_PATH = "/products"
    }
}

Retrofit ๊ธฐ๋ฐ˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ

  • ์‹ค์ œ๋กœ๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ๋˜์ง€๋งŒ, ์š”์ฒญ์— ๋Œ€ํ•œ Retrofit ๊ธฐ๋ฐ˜ ์‘๋‹ต์„ ๋ฐ›๊ธฐ ์œ„ํ•ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
class ProductRepositoryImpl : ProductRepository {
    private val mockProductService: ProductService = MockProductService()

    override fun loadPagingProducts(offset: Int): List<Product> {
        var pagingData: List<Product> = listOf()
        thread {
            pagingData = mockProductService.findPagingProducts(offset, PRODUCT_LOAD_PAGING_SIZE)
        }.join()
        if (pagingData.isEmpty()) throw NoSuchDataException()
        return pagingData
    }
}

์ •๋ฆฌ

  • MockWebServer๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋งค์šฐ ์œ ์šฉํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
  • ์‹ค์ œ ์„œ๋ฒ„ ์—†์ด๋„ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋‹ค์–‘ํ•œ ์‘๋‹ต ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ†ตํ•ด Retrofit, OkHttp์™€ ๊ฐ™์€ ๋„คํŠธ์›Œํฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋”์šฑ ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.