How To Mock Backend Responses in Your Android Projects

woga·2023년 8월 6일
0

Android 공부

목록 보기
45/49
post-thumbnail

우리는 안드로이드 앱 개발을 할 때 API를 이용해서 개발하는게 많다. 그러다보면 원하는 Data Response대로 앱이 잘 그려지는지, 구동하는지 테스트가 필요하기 때문에 간혹 Mock data를 mocking하여 쓸때가 있다
이 때 대부분 찰스를 쓸 것이다.

출처: 내 맥북

그런데 요런 툴 없이 코드로 서버의 응답을 대체할 수 있는 솔루션이 있다.

프로젝트 내부에 JSON 파일을 넣기 때문에 코드에서 직접 Mock Data Response를 생성하는 단위 및 통합 테스트를 작성할 수도 있다.

물론 Retrofit을 사용한다.

기본적으로 우리가 하려는 것은 네트워크 콜을 가로채는Interceptor를 만드는 것이다. 디버그 모드일 경우, Request 할 때 지정된 JSON을 기반으로 Response를 받을 것이다.
릴리즈 모드일 경우, Interceptor Request 헤더를 제거하고 원래 응답 모델대로 데이터를 받게 할 수 있다.

참고로 이 구현은 Android Studio에 통합된 Network Inspector와 호환되지 않는다.
그래서 실제 서버를 공격하지 않는 한 해당 도구를 사용하여 디버깅할 수는 없지만 request 할 때 헤더에 우리가 넣어준 JSON이 지정되어 있으므로 찾기가 쉽고 어떻게 흘러가는지 파악 가능하다.

코드

Retrofit, OkHttp3 및 DI 라이브러리가 필요하다.
(아래 예시에서는 코인인데 원문에서는 코인으로 쓰고 있어서 그렇다. 대거, 힐트 다 상관없다)

retrofit = "2.9.0"
okhttp3 = "4.10.0"
koin = "3.3.2"

retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp3  = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" }
okhttp3-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp3" }
koin = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
convertergson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }

만약 블로그에서 기사를 검색하고 싶다고 생각해보자.
이를 위해 GET으로 API를 호출하는게 필요하다.

interface MockApiService {
    @GET("articles")
    @Headers("$MOCK_RESPONSE_HEADER: getArticles.json")
    suspend fun getArticles(): List<String>
}

@Headers 애너테이션은 일반적인 Retrofit 호출이지만 태그로 상수와 개발 중에 응답하려는 JSON을 추가할 수 있다.

API 호출이 정의된 인터페이스가 있으면 평소와 같이 Retrofit을 설정하면 된다.

private const val BASE_URL = "https://molidevwrites.com/"
private val interceptor: FakeResponseInterceptor = FakeResponseInterceptor()
private val client: OkHttpClient = OkHttpClient.Builder().apply {
    this.addInterceptor(interceptor)
}.build()
private val retrofit =
    Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
        .client(client).build()

object MockApi {
    val retrofitService: MockApiService by lazy {
        retrofit.create<MockApiService>()
    }
}

여기서 유의해서 봐야할 건 변수 interceptor
FakeResponseInterceptor에는 이런 코드가 있다.

const val MOCK_RESPONSE_HEADER = "MOCK_RESPONSE"
const val SUCCESS_CODE = 200

class FakeResponseInterceptor : Interceptor {

    private val assetReader: JsonReader by inject(JsonReader::class.java)

    override fun intercept(chain: Interceptor.Chain): Response {
        return if (BuildConfig.DEBUG) {
            handleMockResponse(chain)
        } else {
            chain.proceed(
                chain.request()
                    .newBuilder()
                    .removeHeader(MOCK_RESPONSE_HEADER)
                    .build()
            )
        }
    }

    private fun handleMockResponse(chain: Interceptor.Chain): Response {
        val headers = chain.request().headers
        val responseString = assetReader.getJsonAsString(headers[MOCK_RESPONSE_HEADER])

        return chain.proceed(chain.request())
            .newBuilder()
            .code(SUCCESS_CODE)
            .message(responseString)
            .body(
                responseString.toByteArray().toResponseBody("application/json".toMediaTypeOrNull())
            )
            .addHeader("content-type", "application/json")
            .build()
    }
}

앞서서 말한 거처럼 Debug, Release 모드일 때에 따라 다르게 작동하도록 조건문을 넣어놨다.

그리고 위에서 injection해서 쓰인 JsonReader은 아래와 같다.

class JsonReader(
    private val assetManager: AssetManager,
) {
    fun getJsonAsString(jsonFileName: String?): String {
        val content = StringBuilder()
        val reader = BufferedReader(getJsonInputStream(jsonFileName).reader())
        var line = reader.readLine()
        while (line != null) {
            content.append(line)
            line = reader.readLine()
        }
        return content.toString()
    }

    private fun getJsonInputStream(jsonFileName: String?): InputStream {
        val jsonFilePath = String.format("%s", jsonFileName)
        return assetManager.open(jsonFilePath)
    }
}

json 파일은 assets폴더 하위로 넣어두면 파일을 읽는다. 또한, 이 클래스 속 AssetManager는 Android 프레임워크에서 제공한다.

그리고 MainActivity에서는 아래처럼 사용하면 된다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            RetrofitmockresponseTheme {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    var response: List<String> by remember {
                        mutableStateOf(List(1) { "No Articles" })
                    }
                    Greeting("I have ${response.size} articles and the first one is: \n ${response.first()}")

                    Button(
                        modifier = Modifier.size(200.dp, 200.dp).padding(20.dp),
                        onClick = { response = getArticles()}) {
                          Text(text = "Get Articles!")
                        }
                }
            }
        }
    }
}

private fun getArticles(): List<String> = runBlocking {
    return@runBlocking withContext(Dispatchers.IO) {
        return@withContext MockApi.retrofitService.getArticles()
    }
}

Reference

https://molidevwrites.com/how-to-mock-backend-responses-in-your-android-projects/

profile
와니와니와니와니 당근당근

0개의 댓글