CoinList: Testing a real API

Episode #334 | 21 minutes | published on April 5, 2018 | Uses swift-4, Xcode-9.2
Subscribers Only
In this episode we talk about testing requests against a real API. For this we will build an app called CoinList that leverages the Crypto Compare API to fetch stats about crypto currencies.

Episode Links

Writing our API Client

We'll start with a simple class that takes a session in the initializer:

class CryptoCompareClient {
    let session: URLSession

    init(session: URLSession) {
        self.session = session
    }
}

We want to test this class, so we'll also create a test for this as well.

class CryptoCompareClientTests : XCTestCase {
    var client: CryptoCompareClient!

    override func setUp() {
        super.setUp()

        client = CryptoCompareClient(session: URLSession.shared)
    }
}

Modeling the Coin List response using Codable

Back in our client class, we want to write a method that fetches the coins and returns them as parsed objects.

We will write these as closely matching the API response as possible. If we want to deviate from this structure in our application, we will map this response to what we want to work with. This makes our Decodable implementation more straightforward.

There's a lot of information to decode in the response, but we will start with a basic structure to get a working example first.

struct CoinList : Decodable {
    var response: String
    var message: String

    enum CodingKeys : String, CodingKey {
        case response = "Response"
        case message = "Message"
    }
}

Indicating Success and Failure with Custom Errors

For our completion handlers, we'll leverage a couple of enums:

enum ApiResult<T : Decodable> {
    case success(T)
    case failure(ApiError)
}

enum ApiError : Error {
    case notFound    // 404
    case serverError // 5xx
    case requestError // 4xx
    case responseFormatInvalid(String)
    case connectionError(Error)
}

Now we can model our completion blocks like this:

typealias ApiCompletionBlock<T : Decodable> = (ApiResult<T>) -> Void

Fetching Coins

  func fetchCoinList(completion: @escaping ApiCompletionBlock<CoinList>) {
         // we'll write this in a minute
    }

Writing a test for fetchCoinList

Before writing the implementation, let's first write a failing test.

func testFetchesCoinListResponse() {
    let exp = expectation(description: "Received response")
    client.fetchCoinList { result in
        exp.fulfill()
        switch result {
        case .success(let coinList):
            XCTAssertEqual(coinList.response, "Success")
        case .failure(let error):
            XCTFail("Error in coin list request: \(error)")
        }
    }
    waitForExpectations(timeout: 3.0, handler: nil)
}

Running this test we can see it fails as expected.

Fixing the test

func fetchCoinList(completion: @escaping ApiCompletionBlock<CoinList>) {
    let url = URL(string: "https://min-api.cryptocompare.com/data/all/coinlist")!
    let req = URLRequest(url: url)
    let task = session.dataTask(with: req) { (data, response, error) in
        if let e = error {
            ApiResult.failure(.connectionError(e)) -=> completion
        } else {
            let http = response as! HTTPURLResponse
            switch http.statusCode {
            case 200:
                let jsonDecoder = JSONDecoder()
                do {
                    let coinList = try jsonDecoder.decode(CoinList.self, from: data!)
                    ApiResult.success(coinList) -=> completion
                } catch let e {
                    print(e)
                    let bodyString = String(data: data!, encoding: .utf8)
                    ApiResult.failure(.responseFormatInvalid(bodyString ?? "<no body>")) -=> completion
                }

            default:
                ApiResult.failure(.serverError) -=> completion
            }
        }
    }
    task.resume()
}

There is a lot of boilerplate code in here, but we can refactor this as we go. For now, I want to get a working example going as fast as possible.

Side note: I'm toying with a custom operator here -=> that will dispatch these completion blocks on the main queue. Too much magic? Maybe, but it is always helpful to experiment to see what works and what doesn't!

Testing that we are called back on the correct thread

I tend to follow the guideline that API clients should call their completion blocks on the main queue. We can verify this is happening with a quick test:

func testCallsBackOnMainQueue() {
    let exp = expectation(description: "Received response")
    client.fetchCoinList { result in
        exp.fulfill()
        XCTAssert(Thread.isMainThread, "Expected to be called back on the main queue")
    }
    waitForExpectations(timeout: 3.0, handler: nil)
}

Now we have our first 2 tests passing!

Okay, so I want to take a look at a realistic example

of building a live application

and use tests along the way to help us build it.

So, the application is going to be called CoinList,

and it's going to be a simple application

that consumes an API for getting cryptocurrency stats

or lists of coins, things like that.

And so we're going to check Include unit tests here,

because we're going to be writing unit tests along the way.

And we'll go ahead and hit Next,

and I will create that in this folder here.

And I want to start off by having an API client

that consumes the CryptoCompare API.

Now, the CryptoCompare API has a ton of really interesting

data, and it's actually free to use under Creative Commons.

And basically, this is going to allow us to get lists of coins

and give us historical data about price

and other stuff, if we really are interested in that stuff.

And so what this is going to do is basically

there's no authentication necessary.

So if we want to get the price of a given symbol,

we can pass in the symbol that we want price to

and the currencies that we want that in,

and it'll return to us something that looks like this.

There's also the, let's see, Other Info has all the coins,

and this is the one I'm going to start with,

just getting a big list of all the coins.

And so I want to design an API client that can interact

with this API, and we're going to do that with a testing focus.

So, I'm going to go over here to our application target,

and we're going to create a new group here.

And I'm going to call this API,

and then we're going to create a new file here,

and we're going to call this the CryptoCompareClient.

Okay.

So, this guy is going to be a class,

the CryptoCompareClient, and let's say that it

takes in a session, which will be a URLSession,

or perhaps maybe a session configuration.

Not really sure how this is going to play out at the moment,

but we can go with this for now.

So we will create our session,

and then I want to create a new file over here.

And honestly, I like to keep things organized

in my test project along with the other project

so that everything looks nicer and more organized.

So I'm going to have an API group here as well

to match the one in our application target,

and then we're going to create the CryptoCompareClientTests.

Okay, so we're going to import XCTest

and then do @testable import CoinList

to import the items from the other module,

and then we're going to create our CryptoCompareClientTests,

which will inherit from XCTestCase.

Okay, so we're going to need the client,

which will be CryptoCompareClient,

and then we need a setup function here, super.setUp,

that is going to create that.

And this should probably not build,

because it looks like we have compile error over here.

Okay, and so this is one of those scenarios

that I came up with where I fixed the compile error

in my application target, but the, the test case target here

doesn't know about that.

So it doesn't understand

this CryptoCompareClient at the moment.

Now it does.

So this is just one of those cases

where sometimes you have to build multiple times.

Okay, so we need to pass in a session here.

I'm just going to use URLSession.shared for now

so that we can get our client set up,

and then I want to go over to our CryptoCompareClient,

and then we're going to have a func here called fetchCoinList.

And this takes no arguments, but it does need a completion,

and our completion is going to be an escaping closure

that is given back some sorts of results.

So, in the case that this request failed,

we need to be able to indicate that,

and if I succeeded, I want to have some sort of type

that is serialized for us.

So, underneath the API, I'd like to create things

called ResponseModels.

Now, these are often going to be completely separate

from our actual models and our application.

This is something that is going to map directly

to the API's response structure, which may be similar

to what I'm looking for, but not quite fully fledged.

I want my model objects to be completely under my control.

So, I'm going to have some things called response models,

and in this case, we're going to have

a response model called CoinList.

And CoinList can be a struct,

and we are going to make this decodable in a minute.

But let's just take a look and see what this looks like.

We've got a response, a message,

a base image and a link URL.

We've got a data object,

which has a bunch of structure in it.

So, we're just going to start modeling that.

So, response is, we can say response is a string.

And we can say the message is a string,

and notice that the case doesn't match,

and so we're going to have to implement our coding keys,

which is a coding key protocol

and is the raw value type of string.

So, we'll say response = "Response" like that,

and message = "Message."

Okay.

So our escaping closure needs to be given

one of those things,

but it also needs to be given some sort of result.

And I like having a type here called ApiResult,

which is a generic type,

and we want to make sure that T is Decodable.

So it's a generic type of any T that is decodable.

And here, we want to have a case of success

where we've given back the T or a case of failure,

in which case we're given back some sort of error.

And in this case, I want it to be an API error.

So we're going to have an ApiError enum as well,

and the API errors that we might encounter

might be notFound.

It might be serverError.

It might be a requestError.

There's a bunch of different ways

that we can get a response here.

This might be modeling a 404.

This would be modeling a 500,

or any 500-level response.

Any other sort of 400-level response

is going to be a requestError.

There's other types of errors that we might encounter

such as like a connectionError, in which case,

that would be something where we didn't get a response.

So I'm just going to have these implement

the error protocols so that we have a bunch

of different sort of errors that we can model.

And our ApiResult is either going to give us

something that's decodable or an ApiError.

Okay, so now I can have our completion block

be sort of a known type.

So we're going to have a typealias here

called ApiCompletionBlock, and that is going to be something

that takes an ApiResult of T,

which means that this needs to be a T,

and returns nothing.

Okay.

So we need to conform to Decodable here in our typealias,

and then now, this can be an escaping ApiCompletionBlock

of type T, which means CoinList needs to be of type T.

Okay, so actually, CoinList is going to be its own protocol.

So our ApiCompletionBlock is going to be of type CoinList.

Okay, so now it's complaining that our CoinList

doesn't correspond to Decodable, so we just need to add

that there, and I've just modeled two basic properties,

but this will allow us to get to a compiling state

as quickly as possible.

Okay, so when I'm doing this

sort of exploratory API development here,

I want to do it in a way that allows me

to sort of do this incrementally.

I've sort of build a lot of boiler plate code,

stuff that I knew that I needed because I've done

this sort of thing before, but I want to be able to test

that our fetchCoinList, when it is successful,

returns some data that I know about.

And when it is, when it returns some sort of error,

that I get something along these lines.

So let's go ahead and go to our CryptoCompareClientTests,

and we're going to set up that first test.

So this is going to be a func, testFetchesCoinListResponse.

And I want to stress that some of these tests

are going to serve as valuable during development

and will allow us to sort of poke at the code that we're

writing in order to get a result, and once we're done,

some of these tests may actually be deleted.

But this allows us to sort of drive this in a way

where we don't need to even touch any UI right now.

I just want to test that I can model a somewhat complicated

response without having also to make a table view

with table vie cells and stuff like that.

So, here in our CoinList response,

we just have the response and the message,

and the response should be success.

So let's just test that we can actually do that.

So, here, we've got our client.

I want to call client.fetchCoinList.

That's going to give us our result,

and then we can switch on the result.

In the case of success, we're going to have our coinList,

and here, I want to XCTAssertEqual

that the coinList.response is equal to Success.

In the case of failure, we're going to have an error.

And then, we can specify XCTAssertFailed,

or assert, we can just say XCTFail.

And give it a message, and our message

might be something like Error in coin list request.

Okay, so again, with our tests here,

this test is going to be asynchronous, so we need

to have an expectation that we received a response,

and I'm going to fulfill that expectation

as soon as we do receive the response, and then at the end

of this, we need to waitForExpectations with timeout.

I'm going to give a three-second timeout with no handler.

Okay, so we've got the basic structure of our test,

and we want to be able to run this.

Now, I did mention before,

while this is running,

I did mention that I don't want tests to hit the network.

And I think it's okay for the test to hit the network

for small periods of time, but I do want to focus

on how do we get this to not hit the network in the future,

because if we want to put this on a CI server,

or we want to run this test every single time

be we check in code, on, you know, over many, many

test runs, we don't want to hit the API every single time.

So I think this is going to be useful for us to get started,

but we will have to figure out a way

to make this test not actually hit the network.

One big important aspect of that is if we look

at this website, there is information about rate limiting.

So my IP address is going to have

some sort of rate limiting,

and if I hit it too many times, I'll get banned.

So that's another important reason

why you may not want to hit an IP in your test.

Okay, so we can see that we got our first test failure,

but it didn't actually do anything in our test.

So, let's go ahead and go to our CryptoCompareClient,

and we're going to make this test pass

by fetching that request.

So first, I need a URL, and the URL we have from here.

I'm just going to copy and paste that,

and then we need a request, which is going to be URLRequest

for that URL, and we can pass in a cachePolicy

of timeoutInterval if we want,

but for now, I'm just going to do that.

Then we're going to have our session build a dataTask

with that URL and a completionHandler.

Actually, I want to use the one that takes a request

and a completionHandler.

So, we'll have the request passed in,

and then our completionHandler will give us data,

response, and an error, all of which are optional,

so we just need to check all those things.

Before I forget, I'm going to say task.resume

so we make sure that this test,

this request actually makes it out the door.

And so now, we just need to interpret this response.

So if we have an error, then we can bail out.

So here, I want to make the assumption that when I'm

calling this back on the completionHandler,

that it's always on the main thread.

So we may do JSON parsing,

we'll do the network request all in the background,

but the callbacks will always be on the main thread,

and I like making that distinction at a class level

so that I can just, I can make the usage of this client

be a lot simpler.

So, when I need to call back,

I need to call dispatchQueue.main.async,

and then, pass in the block that I want to call back here.

So here, I want to call the completion block back

with a result of error, or rather, failure,

and I need to pass in the fact

that there was a connectionError here.

I should be able to pass in an actual wrapping error,

which can, can help there, and so here,

we'll have our completion failure,

and I can pass in this, this e directly into that failure

so that somebody else can actually inspect this

and see what happened.

Actually, this is a nested type

of ApiError.connectionError(e).

So we've got our first case set aside.

The second case, if we didn't get an error,

then we respect to get a response,

and I'm just going to hardcode that here,

response as HTTPURLResponse,

and then we're going to switch on the status of that.

So in the case of 200, then this is successful,

and we need to actually parse the content.

And in the case of, I don't know, something else,

we could say 400 or 404.

There's a bunch of different other things

that we might want to do.

In the case, for right now,

I'm just going to pass in server errors, everything else,

just for the sake of simplicity.

So here we want to DispatchQueue.main.async,

and then here, basically, you're going to copy this code,

but in this case, instead of being a connectionError,

we're going to call it a serverError for now.

Now, it would be up to us to sort of flesh out

these examples and make sure that when we get a 500,

we receive a serverError; when we get a 404,

we get notFound, et cetera.

Looks like I've got some accidental nesting there.

Okay, so, in the case of success,

we need to actually parse this as JSON.

So we're going to create a jsonDecoder,

and then we're going to try to decode this item.

So we'll say jsonDecoder.decode,

and then we just need the type that we have here,

the CoinList.self, from the data that we got

from this API.

Now, the data, I'm going to assume that we have data.

That's generally a safe assumption to make.

So we're going to assume that we have some data.

I do need to call try, and that's going to return

something that we need to hang onto.

So, if this fails, then we're going to have

some sort of decoding error.

And that's another thing that can happen here.

So if our, in our ApiError, if we have notFound,

serverError, et cetera, we can say responseFormatInvalid,

and maybe we pass in the body string that we have there.

And so in this case, we can, again,

dispatch async with some sort of failure

and say responseFormatInvalid,

and then we need to pass in the bodyString,

in which case, we need to check to say String with data.

We're going to interpret that data

in the UTF-8 string encoding.

And then here, we need to pass in the bodyString,

and if the bodyString was nil, we'll just say no body.

And we probably want to print this error out

in the console there so that we can see what happened

if we did get an error,

because this is going to happen during development.

We will have some sort of decodable implementation

that is incorrect, and that will get caught here,

and we need to be able to see what that is.

So we're going to print that out.

In fact, I'm going to print that out right there.

Okay, so now we've got a coinList here,

and in this case, we can call DispatchQueue.main.async,

and we can call the completion block with success

and pass in that coinList.

Okay, so we've written quite a bit of code here,

and we can refactor this code, but the first thing

I want to do is make sure that we're building.

And I'm going to go back over to our test.

I'm going to make sure that this test passes.

Okay, so we've got our first test to pass,

and this is validating that we've actually gone out

to the network, to the right endpoint,

and we came back and we got a success message.

So now, it's time for us to sort of expand this

and sort of decode more things.

Before I do that, I do want to clean up,

clean this up quite a bit,

because there's some opportunities for refactoring in here.

First thing I want to do is simplify calling things back

on the main queue, because this is happening everywhere,

and I just want this to be a little bit easier to read.

And so for this, I'm going to create my own operator.

So we're going to create an infix operator,

and I'm just going to use some sort of symbol that's like,

call this, call this thing on the main queue.

Once we have that operator defined,

we can create a func down here at the bottom

with that symbol as the function name.

That needs to take a T, and then for the arguments here,

we basically want to take the result here,

this result, and then we want to just call,

take the result and a completion block and then call

the completion block with that result on the main thread.

So, we're going to start off with the result.

That is going to be the ApiResult of that T,

and then the completion block is going to be

an escaping completion block of ApiCompletionBlock

of, again, that same T.

Okay, so if we've done this correctly,

then we can call DispatchQueue.main.async,

and then call completion with the result.

Okay, so we've got our operator defined.

So now we can sort of clean this up a little bit.

We can take our results, in this case, which is a failure.

We can say ApiResult.failure,

and then call that on the completion block.

And we can do the same thing over here.

So we can say ApiResult.success,

call that on the completion block,

send that to the completion block on the main thread,

and so I just kind of like, you know,

simplifying the way that we're calling this out

just to make it a little bit easier for us

to mentally parse this method,

and it's something that we do so common

that this may be a worthy endeavor.

So, we'll have ApiResult like that.

Again, call that on the completion block,

and then the last one.

Okay, so just a little refactoring that cleans up a little

bit of the boiler plate dispatching onto a different queue.

Okay, so we've got that working,

and again, now that we've changed that refactoring,

we need to run our test again

and make sure that we're still in a passing state.

One thing that we may want to do is to insert,

assert that when we call, testCallsBackOnMainQueue.

So basically want to do the same thing here.

Instead of expecting that the result is successful

or failed or whatever, we just want to assert

that we're on the main thread.

So we can say XCTAssert, and then we can say

Thread.isMainThread.

And then we could say

Expected to be called back on the main queue.

Okay, and when we run this test,

we expect this test to pass as well.

And it does.

Okay, so we've got our first test passing,

and we've got our first refactoring sort of done

and added a test to make sure

that we're being called back on the main queue.

The next thing we need to do is flesh out

our CoinList response and assert that we're actually

getting more of the response, including all the coins.

blog comments powered by Disqus