Mocking Out Dependencies

Dependencies often make testing hard. Our code needs to get the status of airports from a remote web service. The response from the service will be different at different times due to the nature of the data. The web service may fail intermittently, the program may run into a network failure, and so on. All these situations make it hard to write unit tests, which should be FAIR, for a piece of code that has to talk to such non-deterministic, and by their nature, unreliable external dependencies. This is where mocks come in.

A mock is an object that stands in for the real object, much like how a stunt person, instead of your favorite high-paid actor, stands in or jumps off a cliff in an action thriller. During normal execution the code under test will use the real dependency. During automated testing, however, the mock will replace the real dependency so the test can be run FAIRly.

To facilitate unit testing of code with dependencies, the test will prepare a mock to return a canned response, attach the mock to the code under test, run the test, verify the result is as expected, and, finally, verify with the mock that the code under test interacted with its dependency as expected.

To learn how to create and use mocks, let’s turn our attention to the method that will get data from the remote FAA web service. Given an airport code, the web service returns a JSON response with the airport status data. Getting this data involves two actions: first, we have to send a request to the service URL and, second, we have to parse the response and deal with any possible errors.

Parsing data is straightforward; given a string containing JSON data, extract the necessary details from it. If there was an error, deal with it appropriately. We can easily write tests for that. It’s getting the data from the URL that makes this feature unpredictable and hard to test. We can mock that part out by using a level of indirection.

We can devise the solution using two methods instead of one. A getAirportData() function can rely upon a fetchData() function. We’ll write the fetchData() function, to get the real data from the FAA web service, later . For now, we’ll leave it with a TODO implementation, by throwing an exception if it’s called. The getAirportData() function can then call fetchData() and parse the response JSON string to extract the data. In the test, we’ll mock out the fetchData() function; when getAirportData() calls fetchData(), the call will go to the mock function instead of calling the real implementation. We’ll program the mock function to return a desired canned response.

Many mocking tools are available on the JVM. For example, Mockito is one of the more popular mocking tools for Java. You can use that for mocking dependencies in Kotlin code as well. However, in this chapter we’ll use Mockk[33] for a few good reasons. First, Mockk is capable of easily mocking final classes, and that goes in hand with the fact that in Kotlin classes are final by default. Second, Mockk offers nice capabilities to mock dependencies on singleton objects/companion objects and also to mock extension functions. Mockk also provides facilities to test coroutines. In short, when we use features that are specific to Kotlin, we can benefit from a tool that was created to deal with those.

Creating an Interaction Test

Let’s start with a test for the getAirportData() method of Airport, where we’ll mock fetchData() to return a canned JSON response. In the test, we’ll verify that getAirportData() called the fetchData() function. This is an interaction test as opposed to an empirical test.

Before we can dive into the test, we have to prepare to mock the fetchData() function. For this, we first need to import the functions from the Mockk library. While at it, let’s also bring along a few more imports we’ll need to create the pre- and post-listeners to run code before and after each test.

In the top of the AirportTest.kt file, after the current imports, add the following import statements:

 import​ ​io.kotlintest.TestCase
 import​ ​io.kotlintest.TestResult
 import​ ​io.mockk.*

We’ll design the fetchData() function to be part of the Airport’s companion object. To mock that function, we’ll have to create a mock of the Airport singleton companion object. We can achieve this in a special beforeTest() function. The pair of functions beforeTest() and afterTest() sandwich each test, so that the code within beforeTest() runs before each test and the code within afterTest() runs after each test. In the AirportTest class, right after the fields and before the init() function, add the following two functions:

 override​ ​fun​ ​beforeTest​(testCase: TestCase) {
  mockkObject(Airport)
 }
 
 override​ ​fun​ ​afterTest​(testCase: TestCase, result: TestResult) {
  clearAllMocks()
 }

In the beforeTest() function, we’ve created a mock of the Airport singleton using the mockkObject() function of the Mockk library. In the afterTest() function, at the end of each test, we clear all the mocks that were created and used. Thus, each test can be isolated and independent of each other.

Now we can focus on the test for the getAirportData() function. In the AirportTest class, let’s add a new test after the previous data-driven test for the sort() method:

 "getAirportData invokes fetchData"​ {
  every { Airport.fetchData(​"IAD"​) } returns
 """{"IATA":"IAD", "Name": "Dulles", "Delay": false}"""
 
  Airport.getAirportData(​"IAD"​)
 
  verify { Airport.fetchData(​"IAD"​) }
 }

In the test to verify that getAirportData() invokes fetchData(), we first mock—using the every() function of Mockk—the fetchData() of the Airport companion object, so that it returns a canned JSON response if the given airport code is "IAD". The format of the canned response is an excerpt from the actual response the web service returns—we can find this by visiting the FAA website,[34] using a browser, with IAD as the airport code.

Once the every() function is executed, any calls, direct or indirect from within the test, to the fetchData() function of Airport with argument "IAD" won’t go to the real implementation. Instead, such a call will result in the canned response that follows the returns part attached to the every() expression.

In the test, after the call to every() to set up the mock behavior, we call the yet-to- be-implemented getAirportData() function. Then we verify that the fetchData() function was called, using Mockk’s verify() function. The success of this call to verify() will imply that the call to getAirportData() resulted in a call to the fetchData() function.

Running the build right now will fail, since the methods getAirportData() and fetchData() don’t exist in the Airport companion object. Let’s implement minimally those two functions to satisfy the test.

 package​ ​com.agiledeveloper.airportstatus
 
 data class​ Airport(​val​ code: String, ​val​ name: String, ​val​ delay: Boolean) {
 companion​ ​object​ {
 fun​ ​sort​(airports: List<Airport>) : List<Airport> {
 return​ airports.sortedBy { airport -> airport.name }
  }
 fun​ ​getAirportData​(code: String) = fetchData(code)
 
 fun​ ​fetchData​(code: String): String {
 throw​ RuntimeException(​"Not Implemented Yet for $code"​)
  }
  }
 }

The getAirportData() function simply calls fetchData(), as that’s the current expectation we have for this function. Since our focus is on getAirportData(), we shouldn’t care about the implementation of fetchData() at this time. Thus, within fetchData() we merely throw an exception that it hasn’t been implemented yet.

Run the build and verify that all tests pass.

That worked, but we have to get to a more useful implementation for getAirportData().

Test for Parsing Data

The getAirportData() function at this point merely calls the fetchData() function. We have to implement code to parse the response from fetchData() to create an Airport instance. We’ll drive that using the next test.

 "getAirportData extracts Airport from JSON returned by fetchData"​ {
  every { Airport.fetchData(​"IAD"​) } returns
 """{"IATA":"IAD", "Name": "Dulles", "Delay": false}"""
 
  Airport.getAirportData(​"IAD"​) shouldBe iad
 
  verify { Airport.fetchData(​"IAD"​) }
 }

The only difference between this test and the previous one is that we verify that getAirportData() returns the expected instance of Airport.

Running the build now will fail, since the method getAirportData() isn’t currently returning an instance of Airport. Let’s implement the code for getAirportData() to use the Klaxon parser—we saw this in Chapter 16, Asynchronous Programming—to create an instance of Airport from the JSON response that getAirportData() receives from fetchData(). To help Klaxon easily parse the JSON data and create an instance of Airport, we’ll annotate the properties of the class with @Json. This will help the Klaxon parser to map the values in the JSON response to the appropriate properties in the object. This annotation is necessary only if the property names within the object are different from the property names in the JSON data.

 package​ ​com.agiledeveloper.airportstatus
 
 import​ ​com.beust.klaxon.*
 
 data class​ Airport(
  @Json(name = ​"IATA"​) ​val​ code: String,
  @Json(name = ​"Name"​) ​val​ name: String,
  @Json(name = ​"Delay"​) ​val​ delay: Boolean) {
 
 companion​ ​object​ {
 fun​ ​sort​(airports: List<Airport>) : List<Airport> {
 return​ airports.sortedBy { airport -> airport.name }
  }
 
 fun​ ​getAirportData​(code: String) =
  Klaxon().parse<Airport>(fetchData(code)) ​as​ Airport
 
 fun​ ​fetchData​(code: String): String {
 throw​ RuntimeException(​"Not Implemented Yet for $code"​)
  }
  }
 }

The properties have been annotated so the Klaxon JSON parser can perform the mapping of JSON properties to the corresponding object properties. The getAirportData() function invokes the fetchData() function, and passes the result of that call to the parse() method of Klaxon. The parse() method’s return type is a nullable type, Airport? in this example. This method will either return an instance that it has created using the given JSON data or it will throw an exception—it never really returns a null. So we can convert the returned value of parse() from type Airport? to Airport, using the explicit type-casting operator we saw in Explicit Type Casting.

Run the build and verify that all the tests pass.

The test we wrote for getAirportData() assumes that fetchData() returned a valid JSON object. But the fetchData() method may not always behave that way. If the airport code is invalid, if an airport isn’t supported by the web service, if there’s a network error, or if Murphy’s Law decides to strike in any other way, then the fetchData() method won’t return valid JSON data. It may return a JSON data which contains some error information instead of airport data, or the method may simply blow up with an exception. If the method returns JSON data with an error, the Klaxon parser will blow up with an exception. Irrespective of whether the parsing results in an exception or we receive an exception from the call to fetchData(), our getAirportData() method needs to handle the situation gracefully. We’ll design our method in such a way that if there’s an exception, it will return an Airport with the given code but with "Invalid Airport" as the name. Let’s write a test for this scenario.

 "getAirportData handles error fetching data"​ {
  every { Airport.fetchData(​"ERR"​) } returns ​"{}"
 
  Airport.getAirportData(​"ERR"​) shouldBe
  Airport(​"ERR"​, ​"Invalid Airport"​, ​false​)
 
  verify { Airport.fetchData(​"ERR"​) }
 }

If the given airport code is "ERR", the mocked-out fetchData() in this test will return a JSON response without any valid airport data (for the purpose of this test, we’ll assume that "ERR" is an invalid airport code—no one would dare fly in to an airport with such a code anyway). Upon seeing the response JSON, the Klaxon parser will blow up with an exception. The test sets the expectation that our getAirportData() method, which internally calls the parser, should return an Airport instance with the given code and an invalid name.

To make this test pass we have to modify the getAirportData() method, like so:

 fun​ ​getAirportData​(code: String) =
 try​ {
  Klaxon().parse<Airport>(fetchData(code)) ​as​ Airport
  } ​catch​(ex: Exception) {
  Airport(code, ​"Invalid Airport"​, ​false​)
  }

The method wraps the call to parse() and fetchData() in a try block, and in the catch block it returns the expected Airport instance that indicates a failure.

Run the tests and verify all the tests are passing.

With the aid of a few tests, we designed the getAirportData() method, but the implementation of the fetchData() method is incomplete. The fetchData() method has to make a network call, and the test for that will be an integration test rather than a unit test. We’ll visit that later, but let’s continue on with our focus on unit testing. Let’s leave the Airport class for now and look at the code for the class just to the left of it in the design diagram in The Code Under Test: the AirportStatus.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.34.146