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
}
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 astaticmethod, 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 astaticdictionary, 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, whichloadImagewill 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 toAFImageLoaderif a specificImageLoaderisn'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 includingimageTransitionand 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
loadImagerather thansetImagebecause in my mind things calledsetshouldn't be doing any network access to set something local.loadto 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
MockImageLoaderinto your test, but as writtenfoodoesn't know about it. We need to "inject" it, which means we need to use some mechanism to getfooto use an image loader we specify. Iffoois astructorclass, we could just make it a property, but since I've writtenfooas a free function, we'll pass it in as a parameter, which would work with methods too. Sofoobecomes:What this means is that as you write tests that use
MockImageLoader, you'll increasingly need to somehow pass aroundImageLoaders 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: