Retrofit + MockWebServer TDD, part 2: Http code and Dispatch, TestObserver for RxJava
Dagger2: Inject your ApiService &. MockWebServer
It's easy by following these steps:
- Create a module: (mine) TestAppModule.kt
- Provide the data/instance: use @Provides
- Insert the new module into the component: (mine) TestRetrofitComponent.kt
- Insert the component into the target class.
Now, it's # 1, TestAppModule.kt,
@Module class TestAppModule { @Singleton @Provides fun giveMockWebServer(): HomanMockWebServer = HomanMockWebServer() }
You'll see, one line. # 2, let's insert it into TestRetrofitComponent,
@Singleton @Component(modules = [ AndroidSupportInjectionModule::class, ApiModule::class, TestAppModule::class // Here ============> New Module ]) interface TestRetrofitComponent : AppointmentAppComponent { @Component.Builder interface Builder { @BindsInstance fun application(application: Application): Builder fun apiModule(apiModule: ApiModule): Builder // Here ============> New Module fun testAppModule(testAppModule: TestAppModule): Builder fun build(): TestRetrofitComponent // Type } }
#3, you must prepare your injector.
interface TestRetrofitComponent : AppointmentAppComponent { @Component.Builder interface Builder { ... } // Dagger injector fun inject(test: MainActivityRetrofitTest) }
#4, Inject the component into the class:
@RunWith(MockitoJUnitRunner::class) class MainActivityRetrofitTest { ... // Dagger vars: api and server @Inject lateinit var testApi: AppointmentApiService @Inject lateinit var mockServer: HomanMockWebServer ... @Before fun setUp() { context = InstrumentationRegistry.getInstrumentation().targetContext val testApp = context.applicationContext as DebugAppointmentApplication? // Dagger component val component = daggerInjection(testApp!!) testApp.setTestComponent(component) // Dagger shot: init all variables component.inject(this) // test api injection assertNotNull(testApi) // test mock server injection assertNotNull(mockServer) ... } fun daggerInjection(app: Application) : TestRetrofitComponent { return DaggerTestRetrofitComponent.builder() .application( app ) .testAppModule( TestAppModule() ) .apiModule( ApiModule( testUrl ) ) .build() } }
Now, it's ready to test.
# Test Data
// test data: // Server setting: port val MOCK_WEBSERVER_PORT = 8080 // test url private val testUrl = "http://localhost:$MOCK_WEBSERVER_PORT/" // api key private var testApikey = "key-1234-key" // user id private val testUserId = "id-5678-id" // user type private val testUserType = "Patient"
# MockWebServer: MockResponse
You can try to enqueue the responses one by one, but I prefer to use dispatcher. I write a function to get the MockResponse automatically. In HomanMockServer,
fun insertResponse(code: Int, filename: String) : MockResponse { return MockResponse().setResponseCode(code).setBody(getJson(filename)) }
For example,
// success response_json/200.json var fileName = "response_json/200.json" var resStatus = mockServer.insertResponse(200, fileName) assertNotNull(resStatus)
# Setup Dispatcher of MockWebServer
Let's set up the dispatcher,
fun SetupMockDispatcher() { }
What do you need to test? It's the Http query, which contains endpoint and query commands with "&" and "=". It's called the query pair. Here are the functions to map the query.
// map http query private fun getQueryPairs(query: String): LinkedHashMap<String, String> { val queryPairs = LinkedHashMap<String, String>() if (query != "") { query.split("&".toRegex()) .dropLastWhile { it.isEmpty() } .map { it.split('=') } .map { it.getOrEmpty(0).decodeToUTF8() to it.getOrEmpty(1).decodeToUTF8() } .forEach { (key, value) -> if (!queryPairs.containsKey(key)) { queryPairs[key] = value } else { if(!queryPairs[key]!!.contains(value)) { queryPairs[key] = "" } } } lge("In getQueryPairs:") lge("apikey: ${queryPairs.get("apikey")}") lge("user id: ${queryPairs.get("user")}") lge("user type: ${queryPairs.get("usertype")}") } return queryPairs } // serve map query fun List<String>.getOrEmpty(index: Int) : String { return getOrElse(index) {""} } // serve map query fun String.decodeToUTF8(): String { return URLDecoder.decode(this, "UTF-8") }
Next, let's code the dispatcher.
@Test fun SetupMockDispatcher() { // dispatcher val dispatcher = object : Dispatcher() { @Throws(InterruptedException::class) override fun dispatch(request: RecordedRequest): MockResponse { val path: String = request.path.toString() val endPoint = path.substringAfter("/").substringBefore("?") val query = path.substringAfter("?") lge ("query: $query") val queryPairs = getQueryPairs(query) try { // endpoint: / if (endPoint == "") { lgi("path: ${request.path}") return mockServer.insertResponse( 200, "response_json/200.json") } // endpoint: /appointment else if (endPoint == "appointment") { lgi("path: ${request.path}") queryTest(queryPairs) return mockServer.insertResponse( 200, "response_json/doctor_appt.json") } // Bad path else { lgi("error: $path") return mockServer.insertResponse( 404, "response_json/404.json") } } catch (e: Exception) { e.printStackTrace() return mockServer.insertResponse( 400, "response_json/400.json") } } } mockServer.setDispatcher(dispatcher) }
I have used Http code: 200, 400, 404. And I test the query.
// test query command fun queryTest(queryPairs: LinkedHashMap<String, String>) { // api key: red flag assertFalse("Api key: Not equal is not passed!", "1234".contentEquals(testApikey)) // api key: green flag assertEquals("User Id: Equal is not passed!", queryPairs.get("apikey"), testApikey) // user id: red flag assertFalse("User Id: Not equal is not passed!", "1234".contentEquals(testUserId)) // user id: green flag assertEquals("User Id: Equal is not passed!", queryPairs.get("user"), testUserId) // user id: red flag assertFalse("User type: Not equal is not passed!", "1234".contentEquals(testUserType)) // user id: green flag assertEquals("User type: Equal is not passed!", queryPairs.get("usertype"), testUserType) }
# Send Request by RxJava - TestObserver
// Get Http response body private fun getData(): MutableList<List<AppointmentDTO>> { // RxJava: val apiObserver = TestObserver<List<AppointmentDTO>>() // send request testApi.getAppointment(testApikey, testUserId, testUserType) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(apiObserver) apiObserver.awaitTerminalEvent(1, TimeUnit.SECONDS) apiObserver.assertNoErrors() return apiObserver.values() }
TestObserver makes everything clear and simple because you don't need to reschedule the Scheduler from asynchronous to synchronous like the older version of RxJava2.
# Test Http 200
@Test fun TestHttpCode200() { SetupMockDispatcher() val results = getData() lge("results: $results") assertThat(results.size, greaterThan(0)) }
I want to make sure that the client gets the body, so the size of result must be > 0. In fact, the test case is only 3 lines after all.
# Run
It's passed. If not, please check the error message.
# Next Part
I will write more tests to check the Http body in the NEXT part. Also, we need to use same technique in real data after the test.
# YourMockWebServer
Here is my MockWebServer:
class HomanMockWebServer { var asset: AssetManager? = null var slow: Boolean = false private val server = MockWebServer() var port: Int = 8080 set(value) { if (value >= 0) field = value } fun startServer() { server.start(port) } fun startServer(port: Int) { this.port = port startServer() } fun closeServer() { server.shutdown() } /* Response functions */ var filePath = "" set(value) { if (value != "") field = value } fun getJson(filename : String) : String { lge("path: ${asset.toString()}") lge("asset: $filename") return String(asset!!.open(filename).readBytes()) } fun listFiles() { var numeric = true val path = asset!!.list(filePath) if (path != null) { for (f1: String in path) { val ss = f1.removeSuffix(".json") var code = "http: " numeric = ss.matches("\\d+".toRegex()) if (numeric) { code += ss lgi("path: $ss; $code") } else { lgi("path: $ss") } } } File(filePath).walk().forEach { lgi("file name: $it") } } val jsonHeader = "Content-Type: application/json; charset=utf-8" fun insertResponse(code: Int, filename: String) : MockResponse { return MockResponse().setResponseCode(code).setBody(getJson(filename)) } fun takeRequest(): RecordedRequest { return server.takeRequest() } fun setDispatcher(dispatcher: Dispatcher) { server.dispatcher = dispatcher } companion object { const val tag = "MYLOG "+"HomanMockWebServer" fun lgi(s: String) { Log.i(tag, s)} fun lge(s: String) { Log.e(tag, s)} fun lgo(s: String) { println("$tag $s") } } }