우리는 안드로이드 앱 개발을 할 때 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()
}
}
https://molidevwrites.com/how-to-mock-backend-responses-in-your-android-projects/