Testable downloaders

In my last post, where I refactored a typical download-and-parse workflow, I said this about the downloader: 

There is still some work to be done to make the downloader testable, though
— Me

I probably didn't write that as correctly as I should. It's not that I should have written the downloader to be more testable, I should have written a test-friendly downloading pattern. The JSON downloader in the previous post can't really be tested, because the golden rule is once you leave your app (in this case, for a network request) you are not longer UNIT testing.

What I would want to do is make sure the code that is requesting downloaded data should be written so that it is not dependent on a specific downloader. If I'm testing I'd like to make my code use a different downloader, which might instead read from a file, or a plist or just make stuff up. That way I avoid the network request.

Ok, so how would I do this? I'd like to have something like this:

And now maybe I'll add a tester downloader

So then I could write a downloader function like this:

So now I can either download correctly, or use the test downloader when I’m running unit tests:

BUT!!!!!!

This doesn’t work. We get the dreaded “Protocol can only be used as a generic constraint….” error you’ve probably seen a thousand times.

To get around this I need to use Type Erasure, which is where everything gets much larger and that’s why this problem is a whole new subject, hence why I avoided it in the last blog post.

I won’t go into the details of type erasure, it’s huge. But you need to create a new struct that implements the protocol and acts as a proxy, it’s just a whole load of complicated stuff. This is a great video explaining it: https://academy.realm.io/posts/altconf-hector-matos-type-erasure-magic/

But the solution is the following, I make the type erased struct:

and make my downloader method using the type erased type:

Now everything’s more testable. 

Additionally I now have scope for multiple testable scenarios. I can have a downloader that fails, perhaps with a given error, a downloader that succeeds with an empty result, a downloader that reads from a file and returns the parsed data, whatever I can think of, this can help test corner cases and failure scenarios that previously would have been left out of testing.