CoinList: Stubbing Network Requests

Episode #335 | 27 minutes | published on April 13, 2018 | Uses swift-4, Xcode-9.2
Subscribers Only
In this episode we implement OHHTTPStubs, a library that can be used to intercept and stub out network calls made with URLSession. Using this technique we can avoid hitting the network for our tests. We can also simulate different responses that are difficult or impractical to simulate in a real request.

Episode Links

Modeling the Coins Response

In the API response we saw last time, all of the coins are returned in a dictionary underneath the data key. We'll start by modeling that.

    enum CodingKeys : String, CodingKey {
        case response = "Response"
        case message = "Message"
        case baseImageURL = "BaseImageUrl"
        case baseLinkURL = "BaseLinkUrl"
        case data = "Data"
    }

We'll provide a custom nested struct to represent this data, decoding with a dynamic CodingKey implementation (since our keys are dynamic):

 struct Data : Decodable {
        private struct Keys : CodingKey {
            var stringValue: String

            init?(stringValue: String) {
                self.stringValue = stringValue
            }

            var intValue: Int?

            init?(intValue: Int) {
                self.stringValue = String(intValue)
                self.intValue = intValue
            }
        }

        private var coins: [String : Coin] = [:]

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: Keys.self)
            for key in container.allKeys {
                coins[key.stringValue] = try container.decode(Coin.self, forKey: key)
            }
        }

        func allCoins() -> [Coin] {
            return Array(coins.values)
        }

        subscript(_ key: String) -> Coin? {
            return coins[key]
        }
    }

This references a new type Coin. I want this to be somewhat isolated from the rest of the application, so this will be a nested type inside of CoinList:

    struct Coin : Decodable {
        let name: String
        let symbol: String
        let imagePath: String?

        enum CodingKeys : String, CodingKey {
            case name = "CoinName"
            case symbol = "Symbol"
            case imagePath = "ImageUrl"
        }
    }

Now that we have this in place, we can write another test that we can successfully parse these coins and access them by their symbol.

Testing that we can parse coins

We'll start by copying the basic structure of making the request and setting up the expectation that we can wait on later:

func testCoinListRetrievesCoins() {
    let exp = expectation(description: "Received response")
    client.fetchCoinList { result in
        exp.fulfill()
        // ...
    }
    waitForExpectations(timeout: 3.0, handler: nil)
}

Then we can write some assertions that we are able to pull out a coin successfully:

        switch result {
            case .success(let coinList):

                XCTAssertGreaterThan(coinList.data.allCoins().count, 1)
                let coin = coinList.data["BTC"]
                XCTAssertNotNil(coin)
                XCTAssertEqual(coin?.symbol, "BTC")
                XCTAssertEqual(coin?.name, "Bitcoin")
                XCTAssertNotNil(coin?.imagePath)

            case .failure(let error):
                XCTFail("Error in coin list request: \(error)")
            }

And if we run this test, it passes. 🎉

But we probably don't want to keep running our tests against a live API, do we?

Setting up OHHTTPStubs

To stub out network calls, we'll use a library called OHHTTPStubs.

We'll integrate this into our test target in our Podfile:

platform :ios, '11.2'

target 'CoinList' do
  use_frameworks!

  target 'CoinListTests' do
    inherit! :search_paths

    pod 'OHHTTPStubs/Swift'
  end
end

Failing tests that hit the network

The first step is for us to not allow any request to hit the network in our tests. We can make exceptions to this rule, but it's a good thing to set up initially.

At the top of our test we'll import the library:

import OHHTTPStubs

Then we can add this code to the setup() method:

override func setUp() {
    super.setUp()

    OHHTTPStubs.onStubMissing { request in
        XCTFail("Missing stub for \(request)")
    }
}

Now if we run our tests they will all fail because they are hitting the network.

Intercepting requests and returning fake data

We will use the curl command in Terminal to fetch the API response and save it to a JSON file.

We can then add a new bundle to our test target called Fixtures.bundle.

Then we can create a class to read these file and use them as the body of stub responses. We'll call this FixtureLoader:

class FixtureLoader {
    static func reset() {
        OHHTTPStubs.removeAllStubs()
    }

    static func stubCoinListResponse() {
        stub(condition:
        isHost("min-api.cryptocompare.com") && isPath("/data/all/coinlist")) { req -> OHHTTPStubsResponse in
            return jsonFixture(with: "coinlist.json")
        }
    }

    private static func jsonFixture(with filename: String) -> OHHTTPStubsResponse {
        let bundle = OHResourceBundle("Fixtures", FixtureLoader.self)!
        let path = OHPathForFileInBundle(filename, bundle)!
        return OHHTTPStubsResponse(fileAtPath: path, statusCode: 200, headers: nil)
    }
}

Then, back in our test, we can set up a stub by calling:

FixtureLoader.stubCoinListResponse()

And make sure we unset it in our tearDown() method to avoid one stub interfering with a different test.

override func tearDown() {
    super.tearDown()
    FixtureLoader.reset()
}

Now we can continue to run our tests, but they will use this dummy response instead of actually hitting the network.

So our coinListResponse needs to model

a couple of additional properties.

One of them is baseImageURL, and this is going to be

a partial URL that all the rest of the coins

will be referenced against

in order to compute the image for each coin.

There's also a baseLinkURL, which we can use as well.

If we look at the CryptoCompare documentation,

and we go over to the all coins list,

we can see that it's BaseImageUrl and BaseLinkUrl like that,

so we can create our cases here,

baseImageURL = "BaseImageUrl," and then we'll do

the same thing for BaseLinkURL like that.

So, we could write a test for this,

but this is basically essentially what we already had,

and I'm not really that concerned with this.

The next key is going to be a thing called Data.

And if we look at that, Data is an object that,

inside of that object, has a list of coins,

and each key, and this is basically a dictionary,

each key in the dictionary refers to the symbol,

and then there's the detail about that coin within it.

So, we need to have some sort of object,

and I'm going to use a nested type here, a Data,

which is also going to be Decodable,

and then we can say that we have a data

on this CoinList response, and the case for data is Data.

So now we've got a data that we can use,

and we need to have a coding key that is,

because the keys here are all dynamic,

the coding key itself can't just be an enum.

It's got to be its own type.

So, what we're going to do here is have a class here

that we're going to call Keys,

and this can actually be, this can actually

be a private struct here called Keys,

and this will CodingKey implementation,

and that's going to require us to implement

a couple of initializers and a couple of properties.

So, here we need to implement one that takes a string value,

in which case we can just set the stringValue

to the value that was passed in.

And then for the intValue, we can pass in the stringValue

as the string representation of that intValue,

that's fine, and then self.intValue = intValue,

and we're not actually using intValue,

but the coding key protocol forces us to implement that.

So, this is the basic struct we need

to have just sort of a dynamic coding key

that can have any type of value,

and then here in out init(from decoder),

the decoder implementation, which throws,

we need to implement this ourself so that we can loop over

all the keys and construct a coin from each one of those.

So, we're going to have to have,

I'm going to make this private for now.

We'll call this coins, which is going to be

a String to a Coin instance.

This coin, I want to make this also an extension on this

CoinList, so we're going to make this a struct Coin here,

because I want my own type later on to be called Coin,

and this Coin is going to be, like,

if we take a look at one of these coins,

notice that the Url is a relative URL,

and the imageUrl is a relative URL.

When I deal with this inside of my application,

I'm going to want that to be a fully fledged URL.

But we can't build that yet.

We have to first parse the response exactly how this is,

and then later, we can translate these or morph these

into our own types that will resemble these types,

but maybe they have a different structure.

So, another thing to consider here is the fact

that there's no price information here,

and we may want some sort of notion of like,

the last coin price.

That may be something that we want to do on our own model,

but it's not returned in the API like that.

So I think it's, it's important when you're dealing

with applications like this that we model

our response models as closely to the API as possible

and then translate those into objects

that we want to work with inside of our own application.

So, with that in mind, our coin is now nested

underneath the CoinList structure,

and that keeps it isolated from the rest of our application.

A CoinList coin will look exactly like this,

but our coin model may look a little bit different.

Okay, so our struct Coin here is going to have a few things.

Let's take the CoinName, the Symbol,

and the ImageUrl.

So, we will have a name, which'll be a String,

a symbol, which will be a String,

and then we'll have the imagePath, which will be a String.

And then we will have our enum CodingKeys to map those.

That's going to be CoinName, and then symbol will be Symbol.

And then the imagePath will be the ImageUrl.

Okay, so now we've modeled our Coin properly.

Now we just need to get the container from the decoder,

and we'll say container(keyedBy: the coding key protocol,

or what we called Keys, like that,

and then we can loop over all the keys.

So we can say for key in container.keys, allKeys.

Then we just need to add our coins here,

so we just need to have a list of these coins.

We can initialize that to an empty dictionary

right up here and just append to it, that might work.

So we can say coins for key.stringValue.

Remember, the key is going to be this PPC,

which is going to be the string value,

and then we want to decode the coin itself here,

so we can say container.decode(Coin.self, forKey: key).

We also want to be able to inspect this type,

and right now, our coin's property here is a dictionary,

which represents what we just parsed,

but I also want to just be able to get a list of all coins.

So what we can do here is have a func called allCoins,

which returns an array of Coin.

And here, we just want to return coins.values,

and then we need to convert that to an array.

And then we might want to have a subscript here

that will take a key which is a String and return

a Coin optionally, and then that can just reach

into our coins array for that key.

Okay, so we've got our data type,

and we've got it compiling, and it looks like it works,

but let's go ahead and take a look at our test.

What I want to test is that we can make a call,

testRetrievesCoins, and I want to say

CoinList retrieves coins.

So here I'm going to again copy this and instead

of asserting that we got a successful response here,

I now want to make an assertion on the actual coins

that we got back, so I can say

XCTAssertEqual(coinList.data.allCoins().count),

and then we're going to assert that we got the appropriate

number of these, and I'm not really sure how many there are.

But it looks like hundreds.

Let's just double-check that we got some.

Actually, we could just say XCTAssertGreaterThan,

and make sure that the coins are greater than one.

So we at least got on, okay?

I also want to make sure that I can get a specific coin,

so in this case, I will say let coin = coinList.data.,

or, indexed with BTC.

I want to make sure that that's not nil.

XCTAssertNotNil,

and then XCTAssert, and we can assert the few properties

that we care about on this one.

We assert that the coin's symbol is BTC.

We assert, and this needs to be AssertEqual,

XCTAssertEqual the coin's name is equal to Bitcoin,

and then XCTAssertEqual,

or I want to say AssertNotNil,

that the coin's imagePath is not nil.

Okay, let's go ahead and run this

and make sure that we can get all the coins

and we can index the specific coin from the list.

Okay, we've got a failure here,

and this is probably a decoding error.

And it could be quite hard to see the actual

error message here because Xcode doesn't actually show it

in a way that I can actually read it,

but if we go over here to the build time errors list,

this actually shows us test failures as well,

and we can see the error in coin list response,

the response format is invalid.

We can't actually read this error message very easily,

but we can see the fact that it's returning the body.

But it didn't actually return the error message,

and that should have been left for us in the console,

so let's go ahead and look inside of the console instead.

Okay, so, we can see here that we got a decoding error

and the path was CoinList.CoinList.CodingKeys.data,

and inside of there, we had a stringValue for ROS,

and there was no ImageUrl in that particular string.

And if we go look at the API in here,

if we scroll down, well, it's not going to be easy

for us to find ROS in there.

But basically, the issue

is that it couldn't find the ImageURL, and we modeled

that as a required property here in our CoinList coin.

So, imagePath here is actually going to be optional,

which will allow it to decode it and leave a nil in there

in the cases where we don't have an image.

So let's go ahead and run this again, okay.

And our test succeeded.

So now we've got a test that validates a little bit more

of this API and it's validating parts of the response,

and then we decided that from JSON properly.

Okay, so I mentioned that it is less than desirable

to have our unit test hit a live API.

On the one hand, it allows us to get started quickly

and actually see that our results are working,

but the downsides are, we could get banned from this API

by running our test too often, and these tests won't work

if we have intermittent connectivity or if we're working

on a train or an airplane or something.

And so, having these hit the live API

is not desirable.

So what I want to do is tackle that problem next

before we go any further.

To do that, I want to implement a library

called OHHTTPStubs, which we can get through CocoaPods.

So I'm going to create a pod file in this folder,

and then we will open that up,

and I'm going to set our platform to 11.2, I think,

which is the version that we're using,

and then, let me get rid of these comments.

Okay, so in our pod file, notice that our target

for CoinList is the outer target, and then we can add

our pods in here, like if we have a pod

that we want to use here, we can use that,

but then there's a nested one for our tests.

And so we can have separate pods just for testing.

In this case, I want to use OHHTTPStubs.

And this particular project has an Objective-C version

and a Swift version, so we're going to use the /Swift pod,

which is only going to include the Swift-related things.

Okay, so with that added, I'm going to save and quit,

and we're going to run pod install.

And this is going to download the pod,

integrate it with our project, and I actually need

to close Xcode, and I want to open up the workspace instead.

I've got a shortcut for that, xc, which is going to open up

the first workspace it finds in this folder,

and then we have got some warnings here

that I want to take care of now,

because I don't want to leave these alone.

So in our Debug target for our tests,

Debug and Release target for our test project,

we have this ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES set,

and I need to unset that.

So let's go ahead and go into the build settings,

and this always embeds Swift standard libraries.

I'm just going to hit delete, which is going to make that

not bold, which means it's back at the default setting.

And to verify that I did fix that warning,

I want to run pod install again and just make sure

that we don't get those warnings again.

Okay, looks good.

Okay, so at this point, now we've got a pod in here,

OHHTTPStubs, and we can use this to stub out the networking

or the actual response that we got from an API

and allow us to get a consistent response back.

And this is also useful if we want to hit the API,

and then we want to, we expect to get back

an error state, so we can actually model and fake out

getting back an error state from the API,

which may be something that's hard to set up in real life.

And so now that we have that OHHTTPStubs,

we're going to go back into our test project,

and we need to set up a way to stub out calls to this API.

Okay, so the first step I need to do is import

OHHTTPStubs at the top of my test,

and then in the setup for my test,

I want to say OHHTTPStubs.onStubMissing,

and I want to fail a test any time a network call is made

and the stub isn't present.

So here, we're given the request that was made,

and we can see something like XCTFail with the message

Missing stub for, and then say \(request).

Maybe we just output it like that.

And so this is going to hook into the networking system,

and basically reroute requests through OHHTTPStubs,

and this is a useful mechanism for allowing us to interact

still with URLSession and the foundation APIs

for networking, but then intercept those at a specific point

and say I want to return this response

or I want to return that response.

For now, I just want to have anything, a big safety net

that says nothing should go

out of the system without us saying it's okay.

So now if I run the test, I expect them all to fail,

because we're missing those stubs.

So it tells us we're missing a stub for that particular

endpoint, and notice that all three tests are failing.

This means they're not hitting the network anymore;

they're all failing right here.

Okay, so, what we need to do is stub a specific request.

So I want to go over here, and I want to interact

with that API and pull down a live response.

So we're going to copy this URL here,

and then I'm going to go back to the terminal,

and I'm going to use the curl command to pull down this data.

So, this is a lot of data, and we can actually

pull that down into a coinlist.json file,

and we can do this for any of the responses that we want,

and we can even edit this file if we want to.

And now I want to bring this file into my test target

underneath something called a bundle.

So we're going to add a new file here,

and I want to scroll down to where it says Settings Bundle,

so I'm going to use that, and we're going to call this Fixtures.

So, test fixtures are basically sort of defined responses

that we can use, and we can actually

delete the stuff that's inside of that already,

and then we can start adding our own files.

So, I'm going to drag in a file

that I already have made from another project,

and we're going to copy these over

and make sure it's added to our test target.

So now we've got a fixtures bundle

that has this CoinListResponse.

I'm not going to click on it here,

because Xcode does not do well with large .json files,

so it may act a little bit strange.

But then I've got some other responses

that I might need later on.

So, now I've got a fixtures bundle

that has the stubbed response that I want in it,

and we just need to load that.

So, I want to create a new group here called Support,

and this is going to be like supporting stuff for my tests.

And we're going to create a new class called FixtureLoader.

We're going to import OHHTTPStubs,

and we're going to have this class FixtureLoader.

So we need to set up our fixture loader such that in a test,

we can say I want you to load this fixture

for this type of request, and then at the end of the test

in the tearDown method, I want to be able to reset those.

So let's start with the reset method,

and I'm just going to make these a static func reset.

That's going to call OHHTTPStubs.removeAllStubs.

So at the end of every test, we're going to remove all stubs,

and then we want to have some static functions

to stub the things that we want.

So in this case, I want to stub the coinListResponse,

and the way this works is we need to tell it

when to stub that particular response.

If you have other networking things in your application,

say you have something like Crashlytics,

it's going to make a call to the Crashlytics API

when it first launches, then you don't necessarily

want to stub those responses.

You only want to stub the ones that you're working with,

and you may want to allow certain ones to go through.

So OHHTTPStubs provides this method stub,

and it's got a condition and a response.

So the condition is a block,

and the response is also a block.

So, if we look at the condition,

we can say I want to look at the request

and then return a true whether I want to stub this thing.

So, if we look at the request, it's just a URL request,

so we can check the URL,

and we can check the host parameter of the URL.

We can check the path and things like that.

And there's actually some really easy helper blocks

that correspond to this interface.

One of those is isHost, and then basically, you want to say,

you could say isHost and then pass in a string.

So in this case, it would be min-API.cryptocompare.com.

And so that thing returns a block that is sufficient

to go in this stub condition argument here.

It's a little bit hard to describe,

but basically, if we take a look at isHost,

note that this does return that same block,

and then we also want to check,

is this the right path?

And in that case there's an is, isPath parameter here,

and we can check to see

if that is data/all/coinlist like that.

And you can chain these together with &&,

because that operator's overloaded to evaluate

both conditions with the provided request.

Okay, so we have our condition here,

isHost and isPath, and then in this case,

our response is going to be some sort of response block.

So we're given the request,

and then we need to return a response.

Going to be loading stuff from this fixtures bundle a lot.

I'm going to create a private static jsonFixture with filename,

and then that is going to return an OHHTTPStubs response.

Basically this same response,

we're going to return that from this method,

because I want to be able to say, return jsonFixture,

and then with the filename,

and the filename in this case was coinlist.json.

We'll add the func keyword there.

So, loading the jsonFixture here is going to rely

on a few helper methods from OHHTTPStubs.

So, we're first going to get a path to the bundle,

which is OHResouceBundle, and the bundle base name here

is Fixtures, and then inBundleForClass

is going to be FixtureLoader.self.

So, whatever bundle the FixtureLoader class is defined in,

the fixtures are going to be loaded in that same way.

Now, once we have that bundle,

we can get the path, which is OHPathForFileInBundle,

and we can pass in the file name and the bundle we just got.

And in this case, we want to force unwrap that,

because I'd like this to crash if the files aren't on disk,

and then finally, we can return an OHHTTPStubsResponse,

and this is going to take a handful of parameters.

The data, the status code, the headers,

basically anything that we want our response to look like.

So in this case, we can grab the file at this path.

statusCode I'm going to assume is 200 in this case,

and headers are going to be nil.

And until we decide that we want to stub

a different type of response, we can do that.

And of course it's looking for a non-optional path,

so let's go ahead and force unwrap that as well.

So, again, if we pass in the wrong path here,

it's going to blow up, and we're going to fix it.

Okay, so, what we need to do now is call

this stubCoinListResponse in our tests when we're looking

at our API tests here, in our CryptoCompareClientTests.

We need to call FixtureLoader.stubCoinListResponse,

and then we also want to have a tearDown method

which calls FixtureLoader.reset.

And the idea here is we don't want to have any loaded fixtures

or loaded stubbed response hanging around for the next test.

Okay, let's go ahead and run this

and see if we get a passing test.

Okay, and we do get a passing test, and if we were

to turn our Mac on Airplane Mode and run our tests,

they would still work.

So these tests are still going to work

on the bus or an airplane.

They're going to work on a CI environment,

and they're not going to count against our rate limit

when it comes to consuming that API.

And the downside of this approach is now we have

this sort of fixed response, and if the API ever changes,

for instance, if they add new coins,

or let's say they add a new attribute,

I'm going to have to delete this .json file

and refresh it with a new one.

And I think that is a small price to pay,

because that's not going to happen very often,

and we're going to run these tests very, very often.

So, this is definitely a massive improvement in testing,

making sure that we don't actually hit the network

that we test with known states and things like that.

One additional case here, and that is that

we want to be able to stub things that return errors.

So if we go back over to fixture loader,

I want to have a coinListResponse that throws an error.

So we're going to create a static func,

stubCoinListReturningError, and let's just say

the server had some sort of error there.

We're going to have the same sort of stub

condition requirement, and then the block for what to do,

in this case, we can return an OHHTTPStubs response

that has Data, statusCode, and headers.

Oh, in this case, let's say that we have the data

is going to be Server Error,

and we can say data(using: .utf8) string coding.

We can pass in this data, say that that's a status code

of 500, and then say that there are,

an empty header's returned.

So now we've got a case where we can say I want to do

the exact same request to the same path, but this time,

I want to return an error.

And what are we going to validate in this case?

I want to validate that when I fetch the CoinListResponse,

CoinListResponse returns ServerError.

So, in this case, it's going to be a very similar test here.

So we've been copying and pasting a lot of code in our test,

and certainly, we could stand to refactor these

to make them more readable.

But I also think it's important to, to stress that

sometimes, you want duplication in your test,

because you don't want the test to share a lot of state,

and I want to make sure that the test

is still easily readable from start to finish.

So, in this case, we want to check to make sure

that the result is not success.

So if we get a successful result,

we actually want to fail,

and in this case, we want to assert that the error

is of a given type.

So we could say if case let, or if case.serverError = error,

and that probably should be ApiError.serverError,

then we can XCTFail here, "Expected a server error

but got \(error)" like that.

We don't care about the coinList property there.

So we can use an underscore, and the serverError here,

if we jump to the definition there,

we could pass in the status as an Int,

if that were an important piece of data.

And so down here when we're setting the serverError,

we can return HTTP.statusCode in there,

and then here, we can say let status, like that.

And now can just say XCTAssert that the status was set.

So status, we expect that to be 500.

Okay, so now we've got the error

that it should have returned an error but it didn't.

That's 'cause we're expecting one,

but we never set up the stub.

So in this test, we can say

FixtureLoader.stubCoinListReturningError,

and when we run the test again, this time,

the error stubbed response should take precedence,

and now we've got a passing test that tests,

did we receive a serverError,

even if the actual server's not returning an error.

Okay, so that's how we use OHHTTPStubs to stub out

network calls but still allow us

to make assertions against our networking code.

blog comments powered by Disqus