Testing your Network logic

Marcel Pintó
The Startup
Published in
5 min readJul 22, 2018

We learn to abstract the business logic to make it testable; we don’t question anymore that ViewModels or presenters should be unit tested, but what if I say that there is more logic? What about your Network logic?

Network logic can get complex…

Nowadays, Android developers use a common setup, Retrofit with OkHttp and Moshi or Gson.

val moshi = Moshi.Builder().build()
val okHttp = OkHttpClient.Builder().build()
val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttp)
.baseUrl("Some url")
.build()

Then some requirements start to come in

  • Track Network events.
  • Add custom headers for your backend.
  • Refresh mechanism for an Authentication Token.
  • And more…

We benefit from OkHttp mechanisms, like interceptors and authenticator.

val level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
val loggingInterceptor = HttpLoggingInterceptor().setLevel(level)

val okHttp = OkHttpClient.Builder()
.addInterceptor(authTokenInterceptor)
.addInterceptor(loggingInterceptor)
.authenticator(authentiactor)
.build()

Then we put all together and hope it works.

We could add unit tests for each interceptor or authenticator, but are we sure the combination and setup of the full network layer are working?

Did we ever wonder what happens when we add one interceptor before the other one? Or we change the order of the Adapters?

Order matters!

What if we could test all the setup and network layer without hitting the Server?

WebMockServer to the rescue.

OkHttp does not only offers the client library. If we check the repo we can find other components. One of those MockWebServer.

This library makes it easy to test that your app Does The Right Thing when it makes HTTP and HTTPS calls. It lets you specify which responses to return and then verify that requests were made as expected.

The README section has a nice explanation and some examples but below I will explain who to use it to test our setup.

First, let’s explain the different concepts:

  • MockWebServer: a lean class that will intercept the HTTP request and replies according to the configured responses.
  • MockResponse: configurable OkHttp Response. (i.e. set HTTP code, body, headers…)
  • MockWebServer.enqueue(mockResponse): Adds a MockResponse to the queue, the first request will be replied with the first enqueued response, second with the second response, and so on…
  • MockWebServer.takeRequest(): Awaits for the next HTTP request, records it, and returns it into a RecordedRequest.
  • RecordedRequest: a class that contains the recorded fields from an HTTP request (i.e. headers, body, method, path…)

That’s most of the things we need, for now, so let’s set up our first test.

A good practice is to abstract the common setup explained above into a single class, that can be used to generate our API interfaces, something like this:

This class is going to be used by any component that requires an API.

Next, we can set up our test class:

  1. Create the MockWebServer.
  2. Use the MockWebServer to generate the URL so any request will be forwarded to the mock server.
  3. Use the real implementation of your interceptors and authenticator.
  4. Use mocks as dependencies for your interceptors and authenticator to verify them.
  5. Create a simple TestApi and TestData to test the setup.

Now that we have our setup ready we need to define what to test.

  1. Parser: we want to make sure that our Moshi setup is correct
  2. Interceptors: we want to make sure that when a request is done our interceptors are behaving as expected
  3. Authenticator: we want to make sure that when a 401 response is returned our Authenticator tries to refresh the token and retry to original response.
  4. (Optional) “Real” APIs interfaces: we could test all our endpoints and APIs with some fake data so we can verify the parsing of all our response and request bodies

Testing the parser

The following code shows a simple parsing test example that verifies that our Moshi and retrofit are properly configured.

@Test
fun `When succeed with valid data, Then response is parsed`() {
val testData = TestData("hello!")
val testDataJson = "{\"name\":\"${testData.name}\"}"
val successResponse = MockResponse().setBody(testDataJson)
mockWebServer.enqueue(successResponse)

val response = testApi.test().execute()

mockWebServer.takeRequest()
assertThat(response.body()!!, is(testData))
}
  1. Creates a success MockResponse with some JSON data
  2. Enqueues the response
  3. Calls the API endpoint (HTTP request)
  4. Waits till the request is processed
  5. Asserts that the response body is properly parsed into our Data class

Testing the Interceptors

The next test will check that our AuthInterceptor is adding a Bearer token header into our requests.

@Test
fun `When a call is done, Then auth header is added`() {
val token = "Token"
tokenStorage.setToken(token)
mockWebServer.enqueue(successResponse)

testApi.test().execute()

val recordedRequest = mockWebServer.takeRequest()
val header = recordedRequest.getHeader("authorization")
assertThat(header, is("Bearer $token"))
}
  1. Set the expected token into our storage (used by AuthInterceptor)
  2. Enqueue the response
  3. Execute and wait for the recorded request
  4. Assert that the header “authorization” is present and contains the expected token

Testing the Authenticator

The Authenticator can be used for several situations (see OkHttp Recipes section), in our case is responsible to request a new token when the Server reply with 401.

This might be the most crucial part of our setup and most difficult to test since we want to make sure that users don’t get log out and are able to refresh the auth token without noticing.

@Test
fun `When fails with 401, Then authenticator refreshes token`() {
val invalidTokenResponse = MockResponse().setResponseCode(401)
val authResponse = AuthResponse(
accessToken = "New token",
expiresInSeconds = 0,
refreshToken = refreshToken
)
val responseBody = moshi.adapter(AuthResponse::class.java)
.toJson(authResponse)
val refreshResponse = MockResponse()
.setResponseCode(200)
.setBody(responseBody)
tokenStorage.refreshToken = refreshToken

// Enqueue 401 response
mockWebServer.enqueue(invalidTokenResponse)
// Enqueue 200 refresh response
mockWebServer.enqueue(refreshResponse)
// Enqueue 200 original response
mockWebServer.enqueue(successResponse)

val response = testApi.test().execute()

mockWebServer.takeRequest()
mockWebServer.takeRequest()

val retryRequest = mockWebServer.takeRequest()
val header = retryRequest.getHeader("authorization")
assertThat(tokenStorage.token, is(authResponse.accessToken))
assertThat(header, is("Bearer ${oAuthResponse.accessToken}"))
assertThat(response.isSuccessful, is(true))
}
  1. Setup the fail response with a 401 code
  2. Setup the refresh response with the new Auth Token
  3. Enqueue the ordered set of responses
  4. Execute and wait for all three requests
  5. Check if the header has the new token and if the token was stored correctly

We could go deeper and deeper into different edge cases like:

  • When refresh call fails, Then original request fails with 401.
  • When refresh call fails with 401, Then refresh call is not called again.
  • When multiple calls fail with 401, Then only one refresh call is done.

Conclusion

Testing the network is not difficult with MockWebServer, the network layer is a really important part of your application, the server side is unpredictable and is difficult to verify all the cases doing manual testing.

Takeaways

  • Abstract the creation of your network components.
  • Test your network layer using MockWebServer.
  • Create on setup the MockWebServer (it will start automatically).
  • Create the base URL with the instance of the MockWebServer
    (val httpUrl = mockWebServer.url(baseUrl))
  • MockResponse comes with an empty body, does your system handle that?
  • Enqueue and TakeRequest are by default ordered, use your own Dispatcher for custom behavior.
  • Test all your APIs or create Test APIs.

This story is published in The Startup, Medium’s largest entrepreneurship publication followed by 348,974+ people.

Subscribe to receive our top stories here.

--

--

Marcel Pintó
The Startup

Android and Flutter expert, founder of https://pibi.studio a mobile experts hub for building apps and providing expertise. Other projects: https://namewith.ai