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