On Testing 3rd party frameworks without losing your hair š¤Æ
Image by Jan VaŔek from Pixabay
Weāve all been there: youāre on your typical developer day when suddenly a wild requirement appears! You proceed to wage the pros and cons of going for a in-house approach against using a battle tested framework by the community. Finally the later one wins and thus the development time for the feature gets shorter by a significant amount. However, how do we test something thatās out of our control? š¤
Testing behavior, not functionality
Short answer is: we donāt, thatās why we went down the road of a community adopted framework. But make no mistake my friend, Iām not saying just because we selected a third-party framework that testing is out of the picture (far from it actually); what I meant is that given that the source code of it is black box for us, what we should aim to do is testing the interactions our project has with it produces in fact what we expect to. Letās give it some more context so we donāt just keep this conversation in the imaginary abstract.
Letās get a couple of things straight for starters
Disclaimer I: since third party dependencies integration to a swift project is not the main topic of this article, Iām assuming you know how to do it (there are many way to achieve this, manyā¦ many ways). Hence the starting point of this piece will be with Google maps already implemented in the project. There are plenty of posts out there explaining step by step the how and why of each, again: thatās not the main focus here.
Disclaimer II: some may look at this and complain I didnāt choose Appleās native map support and therefore Iām getting myself into trouble to which I reply with a couple of arguments.
- The same principles Iāll expose here apply as well to first party framework: theyāre all black box to use and we must test interaction/behavior rather than functionality. Apple themselves recommend to do so.
- I live and work for a Latin-American company that relies heavily on mapās accuracy to provide optimal UX and Apple havenāt payed too much attention to this side of the world with its MapKit support so there you have it: GoogleMaps for the win šŗ š¤ŗ.
Our none cooperative requirement
Say youāre asked to show a map with some points of interest of yours (markers) in it and the marketing team wants to promote our appās commercial effectiveness by showing to our partners how many tourists check their locations out -i.e.: number of clicks each one of them get-.
This looks pretty straight forward:
- Show a map.
- Load the partnerās customized markers .
- Track userās tapping on each of them by leveraging proper delegation exposed by the framework.
So far so good, āwhatās the problem with this code Mauri?ā you might be wonderingā¦ Letās put a simple test in place to see what I mean:
We can use a spy here to assert proper tracking when the interaction occurs. However the first code smell is in line 14 where weāre forced to interact through Google mapās delegate directly (actually from line with the frameworkās import
statement). This implies either leaving our map view in the view controller publicly accessible or creating a dummy one in the spot in order to satisfy the caller requirement. Not only does this sound cumbersome at best (irresponsible and sloppy at worst) but by definition unit testing should happen in small isolated chunks of code without external dependencies (data base queries, network calls or in this case third party frameworks interactions). The very fact that we must add import GoogleMaps
in our tests is a very strong red flag.
Furthermore, when we take this one step deeper and leave our tests at the very bare minimum by stripping the Appās main delegate from being called altogether we get this beautiful error:
Itās a no brainer to solve it but when youāre working on a decentralized architecture where each feature is its own separate module, you donāt have the luxury of your own AppDelegate
and it becomes a roadblock. Again you might be thinking āyouāve painted yourself into a corner by choice there Mauriā but many literature has been written about breaking monoliths and why you (or your team) should generally aim in that direction.
Enough talk, show me the code!
What Iām about to share with you is an approach a fellow colleague of mine who has a lot more years than me in this taught me recently; what we need to do here is applying a facade that will interact on our behalf with the framework. A little bit of context from its wiki
Developers often use the facade design pattern when a system is very complex or difficult to understand because the system has many interdependent classes or because its source code is unavailable. This pattern hides the complexities of the larger system and provides a simpler interface to the client. It typically involves a single wrapper class that contains a set of members required by the client. These members access the system on behalf of the facade client and hide the implementation details.
Letās take a look at how our view controller looks right now:
In red I marked the object to be replaced and in green thereās the delegate to interface with. Letās proceed with the first one:
With this basic facade we accomplish plenty:
- Wrap the main mapās object behind a vanilla
UIView
. - Set camera position by handing it our own custom
MapCamera
object. - Inject a
MapProviderDelegate
which in turn will communicate between the framework and us, achieving dependency inversion this way. - Marker creation handling (this wasnāt view controllerās concern in the first place).
- Finally, thereās the
mapWrapper
object which type cast the instanced view asGMSMapView
one (weāll see why a little bit further).
MapProviderDelegate
is a class protocol with a single method (our current business need demands only this from him but this approach allows flexibility to grow it without breaking compilation). Letās inject this into our view controller to check how it looks:
Itās worth noting that as a default parameter for the object MapViewProvidable
is an instance of MapViewProvider
Wait, is this magic? How does it work?
I had a teacher back at the university who used to say
As engineers, you should know how things work so you donāt think Harry Potter lives in your machines
Letās pick inside MapViewProvider
As we can see hereās where all communication with Google mapās framework and our delegate happens; in red are all external instances and in green our own objects (in fact, this is the only class that needs to import Google Maps). The secret sauce occurs between lines 9 and 13, as soon as the delegateās MapProviderDelegate is set, mapWrapper
is also set to the class itself thus propagating notifications whenever events from the framework are received (as seen in line 34 where only the information we care about is sent instead of an instance of GMSMapView
as before)
Whatās the point of adding this extra layer?
If youāre thinking the purpose of this extra layer is only aesthetics then think again, for starters I hate typing extra code just for the sake of coding. The power of this layer is that its type is an abstraction instead of a concrete implementation (inversion principle all over again) and thus it can be replaced on runtime by something elseā¦ That something can be very well a Fake object. From Martin Fowlerās blog:
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production
This is perfect for us. All we need is to create a Fake object that implements the MapViewProvidable
protocol with just enough code to fulfill compiling requirements like so:
Now our unit tests verify interaction as they should
Conclusion
Dependency inversion and dependency injection might sound as scary terms but whenever someone toast them around in a conversation just know they refer to making properties in a system be interfaces instead of a concrete type (conform to protocols
in Swift) and also be able to pass them from place to place, thus enabling us to replace them by a form of test double when automatic testing take place.
You can check the entire project here. I really hope this strategy serve you guys, I didnāt find too much information related to the specific topic back when I was battling against this which is what ultimately lead me to write this piece (a little contribution back to the community). Until next time šš½
Tools used to make this writing a little bit faster and a little bit prettier:
- Swift Mock Generator: saves you lot of time of mindless mock and spy objects typing
- Nef: awesome tool for generating beautuful documentation
- Skitch: simple image editor to add annotations