How to mock an Apple built-in class

The MKMapItem class is an Apple built-in class. How do I make sure the openInMaps(launchOptions:) method is being invoked on MKMapItem when I test the displayRoute(viewModel:) method?

You’re passing in lat and lng in the viewModel, and then use them to create a Placemark then MKMapItem. This means all inputs are under your control. The output is the fact that the openInMaps(launchingOptions:) method is invoked on a MKMapItem object. When you test the displayRoute(viewModel:) method, you don’t care what the openInMaps(launchingOptions:) method does. You only care that it’s being invoked.

In short, it’s only necessary to subclass MKMapItem and override the openInMaps(launchingOptions:) method to record its invocation. An example test double can be a simple spy as follows:

No surprise here with the spy definition here. But how do you use it? I got this question a lot, to my surprise. In the original displayRoute(viewModel:) method, the mapItem is a local variable, and that’s where many people get stuck. They don’t know how to use the MapItemSpy to replace the real MapItem. Some will overthink and complicate things.

Here’s the simplest solution. Promote mapItem from a local variable to an instance variable so that you can use setter dependency injection to use your spy in your test. Yes, you end up changing your app code a little to accommodate for testing, but that’s a small price to pay for the simplicity.

In the testDisplayRouteShouldOpenMapAndShowRoute() test method’s Given phase, you can create the MapItemSpy and assign it to sut.mapItem.

In the Then phase, you can check mapItemSpy.openInMapsCalled to make sure the openInMaps(launchOptions:) method is being invoked as a result of calling displayRoute(viewModel:).

Raymond
I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

2 Comments

  1. Thanks for the post! However, I don’t think the code in the last snippet will pass the test.
    It’s true that in line 7 of the test, you inject your spy, however when you call displayRoute, your spy will simply get replaced by an MKMapItem (line 9) and openInMaps will be called on that instance, not on your spy object. You have to guard against that by checking if it’s nil or something, which might break the logic of calling displayRoute twice, so you need to be extra careful of that.

    You could use factory injection, but I feel like that might be a bit overkill.

    As a different idea, I’d rather use the wonderful idea from Michael Feather’s book to move the MKMapItem creation into a public method in your SUT, so line 9 would read something like: createMapItem(placeholder: placeholder).
    Then you can subclass your SUT and override createMapItem(placeholder:) and you return a spy from there.

    1. Hi Lordzsolt,

      Have you checked out the follow-up post to this particular problem? It examines 5 different types of dependency injection techniques to see which one(s) can be used to solve this problem.

      The solution you mentioned works, but it also changes the subject under test which is not ideal. As your app and test suite grows, it becomes easier to abuse this technique. As a result, the subject under test can substantially differ from the real thing you’re trying to test. Most of all, it’s super difficult to detect and fix without re-examining all your tests.

Leave a Comment

Your email address will not be published. Required fields are marked *