Retrofit + MockWebServer TDD, part 2: Http code and Dispatch, TestObserver for RxJava

Retrofit + MockWebServer TDD, part 2: Http code and Dispatch, TestObserver for RxJava

Dagger2: Inject your ApiService &. MockWebServer

It's easy by following these steps:

  1. Create a module: (mine) TestAppModule.kt
  2. Provide the data/instance: use @Provides
  3. Insert the new module into the component: (mine) TestRetrofitComponent.kt
  4. 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") }

    }

}

To view or add a comment, sign in

More articles by Homan Huang

Insights from the community

Others also viewed

Explore topics