- 실제 서버 없이도 내가 구성한 ApiService가 응답 처리를 정상적으로 하는지 테스트해 볼 수 있다.
- DTO 매핑이나 세그먼트 초기 구성 테스트에 적합하고, MockWebServer를 활용할 수 있다.
- MockWebServer는 로컬에서 돌리는 가짜 HTTP로서, Retrofit / Okhttp 테스트 시에 임시로 붙어서 동작시켜 볼 수 있다.
- 아래 설정들을 gradle에 추가해준다.
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")
testImplementation("com.squareup.retrofit2:converter-moshi:2.9.0")
testImplementation("com.squareup.moshi:moshi-kotlin:1.14.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
- 안드로이드 프레임워크 없이 가능한 테스트라 로컬 유닛 테스트 하위로 테스트 코드를 작성한다.
- 테스트할 ApiService Interface와 MockWebServer, 그리고 api 호출 상황에 필요한 임시 argument를 선언한다.
class SubwayApiServiceTest {
private lateinit var service: SubwayApiService
private lateinit var mockWebServer: MockWebServer
companion object {
private const val TEST_KEY = "test_key"
private const val STATION_NAME = "강남"
private const val STRANGE_NAME = "안드로메다"
}
- SetUp 코드를 작성해 준다. MockWebServer의 초기화, 가상 JSON 데이터를 data class로 역직렬화하는 데에 사용할 Moshi, Retrofit으로 Service create도 진행해 준다.
@Before
fun setUp() {
mockWebServer = MockWebServer()
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val client = OkHttpClient.Builder().build()
service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create(SubwayApiService::class.java)
}
- 메모리 관리를 위해 After에서 mockWebServer를 셧다운 하는 코드를 선언해 준다.
@After
fun tearDown() {
mockWebServer.shutdown()
}
- 이제 본 테스트 코드를 작성해 준다.
@Test
fun `getSubwayArrivalData returns expected response`() = runTest {
val response = MockResponse()
.setBodyFromFile("subway_arrival_response.json")
.setResponseCode(200)
mockWebServer.enqueue(response)
val actualResponse = service.getSubwayArrivalData(
KEY = TEST_KEY,
statnNm = STATION_NAME
)
assertTrue(actualResponse.isSuccessful)
assertNotNull(actualResponse.body())
val request = mockWebServer.takeRequest()
assertEquals("GET", request.method)
val segments = request.requestUrl?.pathSegments
assertEquals(
listOf(
"api", "subway", TEST_KEY, "json",
"realtimeStationArrival", "0", "20", STATION_NAME
), segments
)
}
private fun MockResponse.setBodyFromFile(fileName: String): MockResponse {
val inputStream = javaClass.classLoader?.getResourceAsStream(fileName)
val source = inputStream?.source()?.buffer()
val jsonString = source?.readString(StandardCharsets.UTF_8)
jsonString?.let { setBody(it) }
return this
}
- MockResponse는 가짜 응답을 생성한다. 이를 위해 반환해 줄 json 임시 데이터를 적용해 준다.
- 보통 서버의 api 명세서에서 가져와서 반영해 주면 된다. 그리고 성공 상황을 체크할 것이므로 responseCode는 200
- 실제 요청 처리 값을 담은 actualResponse를 isSuccessful, body is null 여부를 assert로 검사해 준다.
assertTrue(actualResponse.isSuccessful)
assertNotNull(actualResponse.body())
- 이제 MockServer로 해당 api에 알맞은 Method로 Request를 가져온다.
val request = mockWebServer.takeRequest()
assertEquals("GET", request.method)
- 그리고 세그먼트를 지정하여 실제로 API 클라이언트가 올바른 URL 구조로 호출하는지도 검사할 수 있다.
val segments = request.requestUrl?.pathSegments
assertEquals(
listOf(
"api", "subway", TEST_KEY, "json",
"realtimeStationArrival", "0", "20", STATION_NAME
), segments
)
- 실행 시, 모든 테스트에 검증 성공한 것을 확인할 수 있다.
Build Analyzer results available
오전 12:20:05: Execution finished ':app:testDebugUnitTest --tests "com.windrr.jibrro.data.api.SubwayApiServiceTest.getSubwayArrivalData returns expected response"'.
- runTest는 코루틴 전용 테스트 람다 함수로서 내부에서 suspend api 함수를 호출하기 위해 사용되었다.
runTest { }