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
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.
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
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.