In a real project, most android applications make network requests, and we should be testing these network requests. Initially I started with my network request testing with MockWebServer but ended up using RESTMock instead, due to its useful additional features and simplicity. I really enjoyed using, and it made a lot of my network testing easier.
The RESTMock library is a wrapper around Square’s MockWebServer. It allows you to specify Hamcrest matchers to match HTTP requests and specify what response to return.
Downloading the RESTMock library
To add the RESTMock library to our project by doing the following 2 things:
1. Add the following to your project level build.gradle allproects.repository blick:
allprojects { repositories { // ... maven { url "https://jitpack.io" } } }
2. Add the following to your app level build.gradle dependency block
dependencies { // ... testImplementation 'com.github.andrzejchm.RESTMock:android:0.2.2' // as of this tutorial the latest version is 0.2.2 // ... }
The real code
Without any further ado lets take a look at the class will be testing, how the class is used, and then the unit test it self.
class ProfileInformationProvider(private val api: GitHubApi) { /** * Get the profile information for a username provided * * @param userName the username to get profile information for * @return a [Single] which is notified of the returned profile, or when an error occurs. */ fun getProfileInformationForUsername(userName: String): Single<Profile> { return api.getUserProfile(userName) .subscribeOn(Schedulers.io()) .timeout(15, TimeUnit.SECONDS) // Time out after 15 seconds of no response .observeOn(AndroidSchedulers.mainThread()) } }
Class usage:
private fun getProfileInformation() { profileInformationProvider.getProfileInformationForUsername("some_github_username") .subscribe({ profile: Profile? -> // Do something with the profile information }, { throwable: Throwable? -> // Do something on error }) }
Simple right? I hope. Here we have a class called ProfileInformationProvider with a single function which returns us a users GitHub profile information based on the provided username. The getProfileInformationForUsername function returns an RxJava Single which we can use to subscribe to.
The Unit Test
The test rules
Before we begin testing the class, 2 test rules will need to be created.
- A mock web server rule which runs before every single test and terminates after every test. This rule will restart the RESTMockServer before every test, and then reset the server after the test.
- Since we are using reactive programming for this example (RxJava) a rule to handle asynchronous programming during unit tests. This is not handled by default.
Note: These do not have to be created as separate TestRule classes, they can just be added in the corresponding @Before or @BeforeClass functions, but for cleanliness we will create the separate test rule classes.
First lets take a look at the MockWebServerRule.
/** * Rule runs before and after each test. Required for [RESTMockServer] usage. * * Example usage: * At the top of your class add * * ``` * class ExampleUnitTest { * @Rule * @JvmField * var mockWebServerRule = MockWebServerRule() * * // ... * } * ``` * * @author Josias Sena */ class MockWebServerRule : TestRule { override fun apply(base: Statement?, description: Description?): Statement { return object : Statement() { override fun evaluate() { // Start the mock server before running a test RESTMockServerStarter.startSync(JVMFileParser()) // Evaluate the current running test base?.evaluate() // After the test reset the mock server RESTMockServer.reset() } } } }
The MockWebServerRule initializes the RESTMockServer, evaluates the test (runs the current test), and then resets RESTMockServer the after the test is complete.
Now the RxRule
/** * Runs before a test class, and after the last test in the test class. Required for RxJava usage * during tests. * * Example usage: At the top of your class (in Java), if in Kotlin inside a companion object * * ``` * class ExampleUnitTest { * * companion object { * * @ClassRule * @JvmField * var rxRule = RxRule() * * } * * // ... * * } * ``` * @author Josias Sena */ class RxRule : TestRule { val testScheduler = TestScheduler() override fun apply(base: Statement?, description: Description?): Statement { return object : Statement() { override fun evaluate() { // During a test use the Schedulers.trampoline() instead of the AndroidSchedulers.mainThread() // The reason for this is that AndroidSchedulers.mainThread() is not allowed during tests RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } // During any computation use a test scheduler instead. Prevents errors // thrown when using schedulers like toe Scheduler.io() scheduler RxJavaPlugins.setComputationSchedulerHandler { _ -> testScheduler } try { // Run/evaluate the current test base?.evaluate() } finally { // After the test, reset all schedulers. RxJavaPlugins.reset() RxAndroidPlugins.reset() } } } } }
This rule pretty much allows us to run asynchronous tests in our unit test. Such as network requests. This is not available by default when using RxJava so this is required.
The Actual Test
Now that we have our test rules created in our test class we need to add both of these rules:
class ProfileInformationProviderTest { // ...... @Rule // This is a regular rule which will run before and after each and every test @JvmField var mockWebServerRule = MockWebServerRule() companion object { @ClassRule // This is a class rule which runs only at the begining of the first test, and at the end of the last test. @JvmField var rxRule = RxRule() } // ...... }
For more detail about the difference between @Rule and @ClassRule see the following links or just command click into them in your IDE:
- @Rule: http://junit.org/junit4/javadoc/4.12/org/junit/Rule.html
- @ClassRule: http://junit.org/junit4/javadoc/4.12/org/junit/ClassRule.html
Next lets setup our Retrofit client and GitHubApi, and initialize our ProfileInformationProvider.
class ProfileInformationProviderTest { // ..... @Rule @JvmField var mockWebServerRule = MockWebServerRule() companion object { @ClassRule @JvmField var rxRule = RxRule() } private lateinit var provider: ProfileInformationProvider @Before fun setUp() { // Build the GitHub api val retrofit = Retrofit.Builder() // IMPORTANT! This is how the test url is provided for your test. This is something like http://localhost:55817/ .baseUrl(RESTMockServer.getUrl()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() val api = retrofit.create(GitHubApi::class.java) provider = ProfileInformationProvider(api) } // ..... }
Now we can test our tests! Lets test what a successful response looks like. On a successful (200) response we expect the following to happen:
- Receive 1 profile object for a username provided
- No errors should occur
- That our observer completed
class ProfileInformationProviderTest { private val tokenMatcher = RequestMatchers.pathContains("users/") // ..... @Test fun testGetProfileInformationForUserSuccess() { val mockResponse = MockResponse() .setBody(MockResponses.USER_PROFILE_MOCK_RESPONSE) // The response body .setResponseCode(200) // the response code to return RESTMockServer.whenGET(tokenMatcher).thenReturn(mockResponse) val testObserver = TestObserver.create<Profile>() provider.getProfileInformationForUsername("some_username").subscribe(testObserver) val profile = Gson().fromJson(MockResponses.USER_PROFILE_MOCK_RESPONSE, Profile::class.java) with(testObserver) { awaitTerminalEvent() assertNoErrors() assertValue(profile) assertValueCount(1) assertComplete() } } }
Lets take a look at this together.
- tokenMatcher: This RequestMatcher is what we use to decide what network request was made. You will see how in a sec.
- mockResponse: This is the Mock response that we want to return when this request is made. The mock response allows us to specify parameters specific to our request such as the body, and the response code.
The next thing we do before we start testing our class is tell the mock rest server to return the mockResponse when a GET requests is made that matches the tokenMatcher. Meaning if there is a GET request that contains the path “users/” then return the mockResponse provided.
Taking a look at the rest of the test, we can see what is going on pretty clear. First we create a TestObserver of type Profile. Then we call our providers getProfileInformationForUsername function, and subscribe to it.
USER_PROFILE_MOCK_RESPONSE is just JSON representing a real success response from this network request.
Next we create a Profile object from the expected response value that will be returned.
The TestObserver assertions
Then we assert on our testObserver and make sure what we expect to happen is really happening.
- We start by making sure we wait for any terminal event to occur.
- Assert that there are no errors happening when the network request is made.
- Assert that the value returned equals our mock profile object
- Assert that only one value is returned, and finally assert that the observer is completed.
Bonus
If we wanted to test a timeout during the network request, we can use the TestScheduler available to us in the RxRule we created earlier. If you remember, we added a timeout property to our network request which throws an error if the network requests takes 15 seconds or longer to complete.
@Test fun testGetProfileInformationTimeOut() { val mockResponse = MockResponse() .setBody(MockResponses.USER_PROFILE_MOCK_RESPONSE) .setResponseCode(200) RESTMockServer.whenGET(tokenMatcher).thenReturn(mockResponse) val testObserver = TestObserver.create<Profile>() provider.getProfileInformationForUsername("some_username").subscribe(testObserver) // Jump the mock network request to 15 seconds to cause a timeout to happen rxRule.testScheduler.advanceTimeBy(15, TimeUnit.SECONDS) with(testObserver) { awaitTerminalEvent() // The type of error thrown when a time out occurs assertError(TimeoutException::class.java) // Make sure no value is returned assertNoValues() } }
And that’s it! It is simple as that. The RESTMock library in combination with RxJava makes unit testing reactive network requests really simple and straight forward. You can find the complete source code HERE.
Dependency injection with a library such as Dagger will make testing easier on more complex real life projects, but for simplicity it was explicitly left out.
That is all for now, if you have any questions, comments, or concerns, please do not hesitate to contact me, and remember your tests are only as good as your testing logic. I hope this post was helpful to you.
Thank you for reading.