Alamofire has an extension on UIImageView that makes loading an image very easy. But, for unit testing my code, I would like to mock the result of the response, so that I can test success and failure. How can I mock the .af.setImage(withURL:)
function?
Example code:
imageView.af.setImage(withURL: url) { response in
// do stuff on success or failure
}
2
Answers
The answer from @Chip is very complete and makes it very testable. To make it just a bit simpler, I've created an subclass of the UIImageView, that calls the Alamofire extension. On the places where I was using the
setImage
function, I replaced theUIImageView
with theRemoteImageView
.In my tests, I overwrite the
RemoteImageView
withRemoteImageViewMock
and intercept the calls.This way I can view the calls to setImage and their parameters. And call the completionblock with the correct response to unit test the things that happen in the completionblock.
I think the cleanest way to write tests for code that depends on external frameworks, such as Alamofire, or for that matter, that use I/O, such as network access, is to centralize direct usage of them in a bottleneck that you control, so you can mock that bottleneck. To do that, you will need to refactor your source base to use the bottleneck instead of calling Alamofire directly.
The code that follows does not depend on a 3rd party mocking library, though you could certainly use one if that suits your needs.
Create an API bottleneck
What you want to mock is AlamofireImage’s
setImage(withURL:completion)
method, so that’s the thing you need to create a bottleneck for. You could create an API for loading images into a view from a URL. Since you basically just need to either call Alamofire’s API or some mock, you could use an inheritance-based approach without getting into trouble, but I prefer a protocol approach:It may seem at this point that
loadImage(into:from:imageTransition:closure)
could be astatic
method, but if you do that, mocking will be a pain, because you’ll want to associate an image or failure with a specific URL. With a static method, you’d either have store the associations globally (in astatic
dictionary, for example), which would pollute the mock values across tests, especially if they are executed in parallel, or you’d need to write specific mock types for each test. Ideally you want as many tests as possible to share a single mock type that can be easily configured appropriately for each test, which means it will need to carry some instance data, whichloadImage
will need to access. So it really does need to be an instance method.That gives you your bottleneck that just calls through to Alamofire, but you don’t want your app code to have to explicitly say that it wants to use
AFImageLoader
. Instead, we’ll put using it in an extension onUIImageView
, so we can allow it to default toAFImageLoader
if a specificImageLoader
isn’t specified.I should mention that Alamofire’s actual
setImage(withURL:...)
method actually takes a lot of parameters that have default values. You should probably include all of those, but for now I’m only includingimageTransition
and of coursecompletion
.Refactor your code base
Now you need to replace all the calls to
af.setImage(withURL:...)
in your code base with.loadImage(fromURL:...)
Note since you can now call
myView.loadImage(fromURL: url) { response in ... }
very similar to using Alamofire’s API, it’s a fairly simple search and replace, though you should probably inspect each one instead of doing "Replace All" just in case there is some weird case you have to handle differently.I chose to name the new method
loadImage
rather thansetImage
because in my mind things calledset
shouldn’t be doing any network access to set something local.load
to me implies a more heavyweight operation. That’s a matter of personal preference. It also makes code that is still directly using Alamofire stand out more visually as you refactor to callloadImage(fromURL:...)
Create a mock type for your bottleneck
Now let’s mock it, so you can use it in tests.
Use the mock in unit tests
At this point you’d want to use it to write tests, but you’ll discover that you’re not finished refactoring your app. To see what I mean consider this function:
And suppose you have this test:
Refactor some more, but incrementally this time
You need to introduce a
MockImageLoader
into your test, but as writtenfoo
doesn’t know about it. We need to "inject" it, which means we need to use some mechanism to getfoo
to use an image loader we specify. Iffoo
is astruct
orclass
, we could just make it a property, but since I’ve writtenfoo
as a free function, we’ll pass it in as a parameter, which would work with methods too. Sofoo
becomes:What this means is that as you write tests that use
MockImageLoader
, you’ll increasingly need to somehow pass aroundImageLoader
s in your app’s code. For the most part you can do that incrementally though.OK, so now let’s create a Mock in our test:
You could also test for failure: