8 minutes
Unit Test API Calls With MockWebServer
One of the great benefits of having MVP architecture/Clean architecture is the separation of concerns and the testability that each layer provides. Each layer takes care of things that are specific to it : for example, Presentation layer will take care of things related to presentation logic. For example making calls to Data layer, getting a result and then setting it to the View. Or limiting the amount of data to be shown. Data layer will expose an API which can be consumed by any Presenter and returns the data. The Data layer will contain all logic related to caching (if any), getting data from API if necessary, sanitizing API response (if required) etc. View layer is supposed to be really dumb. It will only intercept clicks/user events and ask the Presenter what to do and then just display whatever the Presenter tells it to display. This separation of concerns is very friendly to writing unit tests since each layer can have mocked dependencies and we can test for happy-cases as well as disastrous ones!
Let’s take a simple example of a screen which shows a list of blogs that are fetched from a remote server. I’m using RxJava2 and Retrofit, OkHttp for this example. There are a ton of other great libraries like Dagger which would help with testability too, but that is out of the scope for this post.
Let’s code!
Your Presenter would look something like this at a bare minimum :
class BlogPresenter(val blogRepository: BlogRepository, val view : BlogView) {
fun blogs() {
val disposable = blogRepository.blogs()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { blogs ->
blogView.setBlogs(blogs)
}
compositeDisposable.add(disposable)
}
}
You have a BlogPresenter which is initialized with a BlogRepository and BlogView which is the contract between the View and the Presenter.
Presenter makes a call to the repositories’ blogs()
method which presumably returns an Observable<List<Blog>>
. In the onNext
method, you set the list of blogs to the view. You add this observable to a CompositeDisposable
and then dispose it off in your appropriate Lifecycle event.
This is all pretty basic MVP.
You will probably write a JUnit test for the presenter, which will be something like this :
@RunWith(JUnit4::class)
class BlogPresenterUTest {
lateinit var blogRepository : BlogRepository
lateinit var blogView : BlogView
lateinit var blogPresenter : BlogPresenter
@Before @Throws fun setUp(){
RxAndroidPlugins.setInitMainThreadSchedulerHandler({ Schedulers.trampoline()})
MockitoAnnotations.initMocks(this)
blogPresenter = BlogPresenter(blogRepository = blogRepository, view = blogView)
}
@Test fun testBlogsReturnsList() {
val blogs = getMockedBlogs(12)
`when`(blogRepository.blogs()).thenReturn(Observable.just(blogs))
blogPresenter.blogs()
// Verify view method is called
verify(view).setBlogs(blogs)
}
@Test fun testBlogsReturnsError() {
`when`(blogRepository.blogs()).thenReturn(Observable.error(NetworkErrorException()))
blogPresenter.blogs()
// View is never called
verify(view, never()).setBlogs(any())
}
fun getMockedBlogs(count : Int) : List<Blog> {
val blogs = ArrayList<Blog>()
for (i in 0..count) {
val blog = mock(Blog::class.java)
blogs.add(blog)
}
return blogs
}
}
These are sample two tests that can be written for the Presenter. One which deals with a successful response and one which deals with an error.
This is all well and good. We’ve now added test coverage to our Presenter. So from the Model, View and Presenter : we’re done with Presenter.
What about tests for the Model?
I found that I was particularly lazy when it came to testing network requests, which is a pretty bad thing. Because, as far as I can see, the most error prone part of your application is probably the network request. The server could be unavailable, the request could time out, there could be malformed JSON returned in the response which will throw our TypeAdapters if you’re using Retrofit. The possibilities are endless.
Enter MockWebServer
Thankfully, the great guys at Square who made OkHttp and Retrofit also have a testing library called MockWebServer which is part of OkHttp.
With MockWebServer, you can easily mock server responses including body, headers, status code etc.
Include it in your project by adding this to your build.gradle
file.
testImplementation 'com.squareup.okhttp3:mockwebserver:(insert latest version)'
Brief Overview
MockWebServer has a very simple API which lets us setup a mock server which will intercept our requests and return whatever mocked response that you want it to return.
You can easily create a server with :
val mockServer = MockWebServer()
Start server :
mockServer.start()
Mock a response using a composable API:
val mockedResponse = MockResponse()
mockedResponse.setResponseCode(200)
mockedResponse.setBody("{}") // sample JSON
Enqueue request so that mocked response is served :
mockServer.enqueue(mockedReponse)
You can also do neat things like intercept the request that was made
val recordedRequest = mockServer.takeRequest()
mockedRequest.path // /blogs
Now that we know about MockWebServer, let’s see what our BlogRepository actually looks like.
class BlogRepository(val blogService : BlogService) {
fun blogs() : Observable<List<Blog>> {
return blogService.blogs()
}
}
It takes in a blogService
in the constructor, which is created using the Retrofit instance.
The BlogService looks simply like this :
interface BlogService {
@GET("/blogs")
fun blogs() : Observable<List<Blog>>
}
Now that we know the BlogRepository, let’s start writing some tests. But before that, we’ll have to setup our test so that the mock server can start listening to requests and then shut down when it’s done.
@RunWith(JUnit4::class)
class BlogRepositoryUTest {
lateinit var blogRepository : BlogRepository
lateinit var mockServer : MockWebServer
lateinit var blogService : BlogService
@Before @Throws fun setUp() {
// Initialize mock webserver
mockServer = MockWebServer()
// Start the local server
mockServer.start()
// Get an okhttp client
val okHttpClient = OkHttpClient.Builder()
.build()
// Get an instance of Retrofit
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl("https://api.blogs.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
// Get an instance of blogService
blogService = retrofit.create(BlogService::class.java)
// Initialized repository
blogRepository = BlogRepository(blogService)
}
@After @Throws fun tearDown() {
// We're done with tests, shut it down
mockServer.shutdown()
}
}
Here, we’ve just laid down the groundwork to start writing our test. We have some initializations that we will need to make of
MockWebServer
, BlogRepository
and BlogService
. In our setUp()
method we make these initializations by getting an instance of
OkHttpClient
and Retrofit
and using those to create our BlogService
and finally supplying BlogService
to our BlogRepository
.
We also wrote a tearDown()
function which will be executed after all the tests have finished executing. Here we simply shut down the server that we created and started by calling mockServer.shutdown()
.
Onto some tests
@Test fun testBlogsReturnsListOfBlogs() {
val testObserver = TestObserver<List<Blog>>()
val path = "/blogs"
// Mock a response with status 200 and sample JSON output
val mockReponse = MockReponse()
.setResponseCode(200)
.setBody(getJson("json/blog/blogs.json"))
// Enqueue request
mockServer.enqueue(mockResponse)
// Call the API
blogRepository.blogs().subscribe(testObserver)
testObserver.awaitTerminalEvent(2, TimeUnit.SECONDS)
// No errors
testObserver.assertNoErrors()
// One list emitted
testObserver.assertValueCount(1)
// Get the request that was just made
val request = mockServer.takeRequest()
// Make sure we made the request to the required path
assertEquals(path, request.path)
}
As explained before, we setup a mockResponse
, enqueue the mockResponse
so that it’s served. Subscribe to it with our testObserver
and then we make some assertions saying, there shouldn’t be any error and there should be only one list that is emitted.
Wait wait wait. Where did the getJson() come from?
The getJson(path = "json/blog/blogs.json")
is a utility method which helps us store our mocked responses as JSON files.
In your test
directory, you can easily create a resources
directory which is used to you-guessed-it-right, store resources.
The utility method to actually read the JSON file is something as follows:
/**
* Helper function which will load JSON from
* the path specified
*
* @param path: Path of JSON file
* @return json : JSON from file at given path
*/
fun getJson(path : String): String {
// Load the JSON response
val uri = this.javaClass.classLoader.getResource(path)
val file = File(uri.path)
return String*(file.readBytes())
}
You can keep a lot of this common stuff in a Base class which other API tests can extend.
Simulating Network Conditions
That was great for our happy-case where we get the appropriate JSON back. But what if our server is down? The test for that would be something like this:
@Test fun testBlogsReturnsError() {
val testObserver = TestObserver<List<Blog>>()
val path = "/blogs"
// Mock a response with status 200 and sample JSON output
val mockReponse = MockReponse().setResponseCode(500) // Simulate a 500 HTTP Code
// Enqueue request
mockServer.enqueue(mockResponse)
// Call the API
blogRepository.blogs().subscribe(testObserver)
testObserver.awaitTerminalEvent(2, TimeUnit.SECONDS)
// No values
testObserver.assertNoValues()
// One error recorded
assertEquals(1, testObserver.errorCount())
// Get the request that was just made
val request = mockServer.takeRequest()
// Make sure we made the request to the required path
assertEquals(path, request.path)
}
SocketTimeoutException?
MockWebServer got you covered.
Remember our mockResponse? Just add this :
mockResponse.throttleBody(1024, 1, TimeUnit.SECONDS)
Which basically means only send 1024 bytes per second.
And then, just add this simple line to our okHttpClient initialization
val okHttpClient = OkHttpClient.Builder()
+ .connectTimeout(2, TimeUnit.SECONDS) // For testing purposes
+ .readTimeout(2, TimeUnit.SECONDS) // For testing purposes
+ .writeTimeout(2, TimeUnit.SECONDS)
.build()
The test would look something like:
@Test fun testBlogsReturnsError() {
val testObserver = TestObserver<List<Blog>>()
val path = "/blogs"
// Mock a response with status 200 and sample JSON output
val mockReponse = MockReponse()
.setResponseCode(200)
.throttleBody(1024, 1, TimeUnit.SECONDS) // Simulate SocketTimeout
.setBody(getJson("json/blog/blogs.json"))
// Enqueue request
mockServer.enqueue(mockResponse)
// Call the API
blogRepository.blogs().subscribe(testObserver)
testObserver.awaitTerminalEvent(2, TimeUnit.SECONDS)
// No values
testObserver.assertNoValues()
// One error recorded
assertEquals(1, testObserver.errorCount())
// Get the request that was just made
val request = mockServer.takeRequest()
// Make sure we made the request to the required path
assertEquals(path, request.path)
}
And that’s it!
You can write a bunch of tests like these and simulate similar conditions! This way, you can easily test the Model part of your application and I would argue the most important and error prone part of your app : Network Requests.