10.
Testing the Network Layer
Written by Tori Gonda
You live in a time of the internet, and it’s likely that your app does, too. Most apps connect to an API over the network in one way or another, giving them a network layer. As this is often a critical part of your app, it stands to reason that you should test it. This is what you’ll learn to test in this chapter! Along the way you’ll learn:
- Tools for testing your network layer.
- How to provide reliable test data.
- Important things to know about maintaining network layer tests.
One thing to consider when testing your interaction with data across a network is that, when running your automated tests, you don’t actually want to make a network call. Network calls are unpredictable. A call can fail because of the network connection, the server, the internet service provider and so on. You can’t always know the state of any of these components. You need your tests to be repeatable and predictable, so this dependency on a wobbly network won’t work, here.
There are tools that you can use to test your network layer without hitting the network, which is what you will focus on in this chapter. You will look at three tools to add to your testing toolbox:
- MockWebserver to mock the responses from network requests
- Mockito to mock your API responses
- Faker for data creation
Getting started
In this chapter, you will work on the network layer for an app called Punchline. This is an app that will show you a new, random joke every time you press a button. To start, find the starter project in the materials for this chapter and open it in Android Studio.
Run the app, but you won’t see much yet:
There will be no UI for you to play with until the end of Chapter 11, “User Interface.” Until then, you’ll see your progress made in the form of green tests for the network layer!
The Punchline app has a single call to the network: the one that fetches a random joke. You will test this request three times, each using a different tool. There are a couple of files that you should have on hand before you get started with your tests:
-
JokeService.kt: This is the Retrofit service that you will declare your network requests in.
-
Repository.kt: This file defines
RepositoryImpl. It’s the glue that connects the network layer with the rest of the app. -
Joke.kt: Your
Jokedata model lives here. You can see that it has values for the ID and joke.
Note: Is it helpful, but not required, to have a familiarity with Retrofit for this chapter. To learn more, go to “Android Networking Tutorial: Getting Started” https://www.raywenderlich.com/2-android-networking-tutorial-getting-started.
Creating your test file
You can’t write tests without a place to put them! First off, create your test file. Create JokeServiceTest.kt in app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ punchline without a class declaration. You’ll put all three of your test classes in this file for easy comparison. Notice that this test is under test and not androidTest. That’s right, you don’t need the Android framework to test your network layer when using these tools! They’ll run nice and fast. You can also use MockWebServer in your Android tests if there’s a place you want to use it in an integration test in your own apps.
Investigating the API
Most of the Retrofit boilerplate is already set up for you. You can peek at KoinModules.kt if you’re interested in seeing that set up. What you care about before you test is the specific endpoint you are testing and implementing.
When interfacing with a network, you often have the endpoints and responses prescribed for you. Even if you are helping with or have a say in how they look, they ultimately live outside your app. Just as you often do in your own app, you have API specifications to stick to here.
The call you are implementing is rather simple. You make a call to "random_joke.json", and get a JSON response back. The JSON looks as follows:
{
"id":17,
"joke":"Where do programmers like to hangout? The Foo Bar.",
"created_at":"2018-12-31T21:08:53.772Z",
"updated_at":"2018-12-31T21:36:33.937Z",
"url":"https://rw-punchline.herokuapp.com/jokes/17.json"
}
You only care about the first two properties for this example — "id" and "joke" — and will ignore the rest.
Now you have all the knowledge you need to get started with your test writing!
Using MockWebServer
The first tool you will learn is MockWebServer. This is a library from OkHttp that allows you to run a local HTTP server in your tests. With it, you can specify what you want the server to return and perform verifications on the requests made.
The dependency for MockWebServer is already added in the project for you. You can see it in app ‣ build.gradle as:
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0"
To start, you need a test class. Add this empty class to your test file:
class JokeServiceTestUsingMockWebServer {
}
Setting up MockWebServer
MockWebServer has a test rule you can use for your network tests. It is a scriptable web server. You will supply it responses and it will return them on request. Add the rule to your test class:
@get:Rule
val mockWebServer = MockWebServer()
Now, you’ll set up your JokeService to test. Because you’re using Retrofit, you’ll use a Retrofit.Builder to set it up. Add the following to your test class:
private val retrofit by lazy {
Retrofit.Builder()
// 1
.baseUrl(mockWebServer.url("/"))
// 2
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
// 3
.addConverterFactory(GsonConverterFactory.create())
// 4
.build()
}
In the above, you:
-
Set the
baseUrlon the builder using themockWebServer. This is required when using Retrofit. Because you’re not hitting the network, “/” is perfectly valid, here. -
Add a call adapter. Using an RxJava call adapter allows you to return RxJava streams in your
JokeService, helping you handle the asynchronous nature of the network calls. Don’t worry; you don’t need to be an RxExpert to keep going! -
Add a converter factory. This is so you can use
Gsonto automatically convert the JSON to a nice Kotlin object,Joke. -
Build it!
You then use retrofit to create your JokeService! Add this code:
private val jokeService by lazy {
retrofit.create(JokeService::class.java)
}
You’re all set to start scripting and testing!
Running the MockWebServer
Your JokeService should have a function, getRandomJoke(), that returns a random joke. To handle the asynchronous nature of network calls, you will use RxJava. If you’re not familiar with RxJava, this is all you need to know: getRandomJoke() will return a Single of type Joke. Parties will subscribe to this Single and receive an event when it emits a Joke. RxJava brings a lot of power, but, for the sake of this exercise, you can think of it as a way to perform a callback.
This test for getRandomJoke() will be called getRandomJokeEmitsJoke() to match the described functionality. Add the test function to your class:
@Test
fun getRandomJokeEmitsJoke() {
}
There’s nothing in it yet, but run the test anyway. There’s some interesting output in the console.
The MockWebServer starts up at the beginning of your test and closes at the end. Anywhere in-between the beginning and end, you can script requests, make those requests to get the response, and perform verifications.
Scripting a response
Now that you have MockWebServer set up to receive requests, it’s time to script something for it to return!
There are two ways to set up the JSON to script a response with. One way is to pull in the JSON from a file. This is a great option if you have a long expected response or you have a real response from a request that you want to copy paste in. You won’t use this first way in this chapter, but know that you can place this JSON in a *.json file under app ‣ src ‣ test ‣ resources, then use getJson("file/path.json") from MockWebServer to fetch it. For example, if you had a app ‣ src ‣ test ‣ resources ‣ joke ‣ random_joke.json, you would call getJson("joke/random_joke.json").
Because it’s such a short response and because it will allow you to dynamically build it, you will create your JSON String in your test file.
Start by creating a property with a JSON string at the class level:
private val testJson = """{ "id": 1, "joke": "joke" }"""
Notice the use of triple quotes to create raw strings. By using raw strings for your JSON Strings, you don’t need to worry about escaping characters such as the quotes around the JSON properties.
You have to admit, this JSON is pretty boring, but later you’ll spice it up! For now, it’s time to learn how to use this JSON to script a response!
Add this to your empty getRandomJokeEmitsJoke() test:
// 1
mockWebServer.enqueue(
// 2
MockResponse()
// 3
.setBody(testJson)
// 4
.setResponseCode(200))
Going over these step-by-step:
-
Use the
mockWebServerthat you created before to enqueue a response. -
You enqueue a response by building and passing in a
MockResponseobject. -
Use the
testJsonthat you created as the body of the response. -
Set the response code to 200 — success!
There are many other things you can set on this MockResponse to test different situations. For example, you can set headers and use throttleBody() to simulate a slow network!
One other thing to note, as the name suggests, you can enqueue multiple responses in a row to return with each new request. This could be helpful when you have an integration test that hits multiple different endpoints and combines the results.
Writing a MockWebServer test
Phew! With all that set up, it’s finally time to finish writing your test. Add these two lines to the bottom of getRandomJokeEmitsJoke(). There will be an error at getRandomJoke() because you haven’t created it yet:
// 1
val testObserver = jokeService.getRandomJoke().test()
// 2
testObserver.assertValue(Joke("1", "joke"))
Here, you:
- Call
getRandomJoke()on yourjokeService. By chainingtest()you get aTestObserverthat you can use to verify the value of theSinglethatgetRandomJoke()returns. - Verify that the value that returns is a
Jokeobject with the same values you placed in thetestJsonand enqueued withMockWebServer.
Next step of the TDD process: Write just enough code so you can compile and run your test. Add getRandomJoke() to the JokeService interface with a return value of Single<Joke>:
fun getRandomJoke(): Single<Joke>
Note: When you’re writing tests and need to create a method, property, etc. that doesn’t exist yet, you can use the shortcut Option-Return on Mac or Alt-Enter on Windows to pop up a dropdown with options to auto create it for you.
Build and run your test! As you may have expected if you went through Chapter 9, “Testing the Persistence Layer,” and have some familiarity with Retrofit, you’ll get an error that Retrofit requires an HTTP method annotation for your new method:
Your goal is to make this run, so next you add an annotation! Add the @GET annotation to your JokeService method:
@GET("https://raywenderlich.com")
fun getRandomJoke(): Single<Joke>
The @GET annotation requires a path or URL. When you pass in a full URL like "https://raywenderlich.com" it uses that URL, but if you pass in a path like "joke.json" it uses the base URL appended with "joke.json" for the URL. To make sure you see your test fail, you’re passing in a full URL. Remember, right now you have a response enqueued for any endpoint given with the base URL, so giving it something without the base URL will return an empty response.
Run it and see it fail:
That’s no joke! No wonder, you’re calling the wrong URL. Update the parameter so the test can pass:
@GET("joke.json")
Run it, and you’re all green!
Refactoring your test
You may feel like there’s a code-smell in the way you’re hard coding the values for the ID and joke. Thankfully, you can change that! Because you’re creating the JSON String in the test, you can create it the way you like. Make a constant for the ID and the joke. By putting them outside the test class at the file level you’ll be able to use them in your other tests too:
private const val id = "6"
private const val joke =
"How does a train eat? It goes chew, chew"
Note: Think about how you might use a factory such as in Chapter 9, “Testing the Persistence Layer” to make these random values for each test.
Now use these values to update your testJson:
private val testJson = """{ "id": $id, "joke": "$joke" }"""
And your test assertion:
testObserver.assertValue(Joke(id, joke))
Run your test, and it should still pass!
Maintaining test data
You just set up some test data and wrote some tests. Now, imagine the JSON response had many more properties and you had more endpoints to test. Then, imagine the format of the response changed. Maybe instead of joke as a String, it contained an object with different translations of the joke. You need to make sure you update your tests with this change when it happens. If you don’t test the new type of response, your tests are no longer accurate.
This is one of the difficulties of network tests: How do you make your tests reliable and maintainable? Do you keep a file of your responses and swap it whenever there’s a change? Do you dynamically create your responses? This will differ depending on your needs and may change as you figure out what’s right for your app. You’ll learn even more about this in Chapter 16, “Strategies for Handling Test Data”
Testing the endpoint
You may be having some doubts about that last test. If it will pass with any endpoint with the same base URL, what is it testing? Is it testing that the response is correctly parsed into a Joke object? While it’s important to know your data is represented correctly, MockWebServer does help you test the endpoint too! Next you’ll add a test that the endpoint is correct.
Add the following test to your test class:
@Test
fun getRandomJokeGetsRandomJokeJson() {
// 1
mockWebServer.enqueue(
MockResponse()
.setBody(testJson)
.setResponseCode(200))
// 2
val testObserver = jokeService.getRandomJoke().test()
// 3
testObserver.assertNoErrors()
// 4
assertEquals("/random_joke.json",
mockWebServer.takeRequest().path)
}
Here’s what’s happening:
- Enqueue the response, same as before.
- Call
getRandomJoke()as before, getting a reference to a TestObserver. - You can also use the
testObserverto make sure there were no errors emitted. - Here’s what you’re writing this for! You can use the
mockWebServerto get the path that was requested to compare it to what you expect. There are many other things other than the request path you can test this way!
Run the test, and you’re right! It fails!
Update the @GET annotation once more to make this pass:
@GET("random_joke.json")
Build and run your test. It passes! You now know your JokeService uses the correct endpoint. That’s all you’ll use of MockWebServer for this chapter, but you can see how it is a powerful and robust tool for testing network requests! But what if you don’t need that much detail? That’s what you’ll learn how to do next.
Mocking the service
Depending on your app and your team, it may be enough to know that the service methods are available and you’re using them correctly. This can be done using Mockito, which you first learned in Chapter 7, “Introduction to Mockito.” In this test you will also be concerned with getRandomJoke(), but your test will worry more about its interaction with the respository.
To start, create a new test class to write your Mockito tests in. This class can be in the same file as your MockWebServer test if you like:
class JokeServiceTestMockingService {
}
You technically don’t need to put your new tests in a new test class (as long as the test methods themselves have different names). However, by creating this separate class, it helps keep the topics divided and allows you to run the new tests without the server running behind them.
Next, you need to set up your test subject. Add this to your JokeServiceTestMockingService class. When prompted, import com.nhaarman.mockitokotlin2.mock:
private val jokeService: JokeService = mock()
private val repository = RepositoryImpl(jokeService)
Here, you’re mocking the JokeService, which you then pass into the constructor of RepositoryImpl.
Now, the test you’ve been waiting for! Add this test method:
@Test
fun getRandomJokeEmitsJoke() {
// 1
val joke = Joke(id, joke)
// 2
whenever(jokeService.getRandomJoke())
.thenReturn(Single.just(joke))
// 3
val testObserver = repository.getJoke().test()
// 4
testObserver.assertValue(joke)
}
Here, you:
- Create the
Jokeobject to use in this test. Notice that it’s using the constant ID and joke you set up while writing the MockWebServer tests. - Set up your
JokeServicemock to return aSinglewith thatJokewhengetRandomJoke()is called. - The signature of
getJoke()on the repository also returns aSingle. Use the sameTestObserverstrategy you used before here. - Assert that the repository’s
getJoke()emits the sameJokethat comes from theJokeServiceyou mocked.
You can see this is testing the interactions with the network layer instead of the network calls themselves.
Run your test and see it fail:
What else now but to make it pass! Open up Repository.kt. Change the body of getJoke() in RepositoryImpl to be the following:
return service.getRandomJoke()
Now, you’re calling the JokeService as expected. Run that test and see it pass this time!
With this strategy, you can’t (and don’t need to) test for the endpoint. That means you get to move onto the next tool to learn!
Using Faker for test data
Throughout this chapter, you’ve been using the same boring old test data:
private const val id = "6"
private const val joke =
"How does a train eat? It goes chew, chew"
This is quite a change from the last chapter when you learned to use Factories to create random test data! How would you like to learn another way to generate test data? Now’s the time! Your test will look very similar to the one you just wrote, but with more fun test data.
One library that helps with this is Faker. With this library, you can generate data from names and addresses to Harry Potter and Hitchhiker’s Guide to the Galaxy. You can see the full list at https://github.com/DiUS/java-faker#fakers. The library is already added to the project for you. You can see it in app ‣ build.gradle as:
testImplementation 'com.github.javafaker:javafaker:0.16'
To get started, add a test class to your file just as before:
class JokeServiceTestUsingFaker {
}
Then, your setup is like the one for the last test with one addition:
var faker = Faker()
private val jokeService: JokeService = mock()
private val repository = RepositoryImpl(jokeService)
You include an instance of Faker to use to create your test data this time.
Final test for this chapter! Add this to your new test class:
@Test
fun getRandomJokeEmitsJoke() {
val joke = Joke(
faker.idNumber().valid(),
faker.lorem().sentence())
whenever(jokeService.getRandomJoke())
.thenReturn(Single.just(joke))
val testObserver = repository.getJoke().test()
testObserver.assertValue(joke)
}
Everything is the same as the last test you write except for the creation of your Joke object at the start. For this, you’re using faker, which you instantiated above. Many of the Faker methods return an object that’s part of the library. It’s from these objects that you can get a value. For example, the IdNumber object that’s returned from idNumber() has methods to get both valid and invalid IDs, as well as other forms of ID such as SSN.
Play around with what values you can get from Faker for your Joke. You don’t need to stick with the ID and Lorem ipsum prescribed here.
Run the test, and it passes! Of course that’s because you implemented this feature with the previous test. Take the responsibility to revert the changes you made so you can see this test fail.
Deciding what tools to use
You’ve learned so many tools in this chapter and in the previous chapters. How do you decide which to use? When is a unit test best or when is it time for an integration test? Hopefully, you’re starting to think about how you can mix and match some of these things, such as how you used both Faker and Mockito for your last test.
There are some patterns, many of which you see in this book, that guide your testing decisions. There are also some restrictions from the libraries themselves on how they can be used. Ultimately, you have to figure out what works best for your needs and for your team.
Don’t be afraid to try out new things or think again when something isn’t working. Where are the holes in your tests where you aren’t catching the bugs? What tests are brittle and hard to maintain? On the other hand, what tests have been consistently saving you from deploying buggy code? Watching for these things will help you grow to understand how to make testing work for your app.
Key points
- To keep your tests repeatable and predictable, you shouldn’t make real network calls in your tests.
- You can use MockWebServer to script request responses and verify that the correct endpoint was called.
- How you create and maintain test data will change depending on the needs for your app.
- You can mock the network layer with Mockito if you don’t need the fine-grained control of MockWebServer.
- By using the Faker library you can easily create random, interesting test data.
- Deciding which tools are right for the job takes time to learn through experiment, trial, and error.
Where to go from here?
You’ve come so far! From unit tests to integration, testing so many parts of your apps. You still have a very important part to learn how to test: the User Interface! All the things you’ve been learning are leading up to this point. In Chapter 11, “User Interface,” you’ll learn how to automate those clicks on your screen and to verify what the user sees.