Every application has to deal with third party code sooner or later. Third party code is code you cannot control, it could be a library used for analytics, or even a class of Foundation.
The problem with third party code is that it makes writing tests harder.
For example consider NSHTTPCookieStorage
,
this class doesn't provide any initialization method apart from sharedHTTPCookieStorage()
.
In fact:
NSHTTPCookieStorage
implements a singleton object (shared instance) that manages storage of cookies.
How do you test code that depends on this component in isolation?
One option you have is to use the shared instance and always makes sure any test data you put into it is removed once the tests are finished. This option is slow and error prone.
A better option is to use a protocol to abstract the dependency and gain more control on your tests.
protocol CookieProvider {
func cookieWithName(name: String) -> NSHTTPCookie?
func setCookie(cookie: NSHTTPCookie)
func deleteCookieWithName(name: String)
}
The components depending on the cookie storage will now expect an instance
conforming to the CookieProvider
protocol rather that specifying and actual
class.
class NetworkClient {
let cookiesProvider: CookieProvider
init(cookiesProvider: CookieProvider) {
self.cookiesProvider = cookiesProvider
}
}
In your tests you can implement a test double cookie provider able to return a cookie set by you, and to store a cookie in memory so that you can verify if.
class CookieProviderTestDouble: CookieProvider {
var cookies: [NSHTTPCookie]?
func cookieWithName(name: String) -> NSHTTPCookie? {
return cookies
.filter { $0.name == name }
.first
}
func setCookie(cookie: NSHTTPCookie)
self.cookies.append(cookie)
}
func deleteFooBarCoockie() {
guard let cookie = cookieWithName(name) else {
return
}
deleteCookie(cookie)
}
}
You can then init the NetworkClient
using the test double:
let cookiesProvider = CookieProviderTestDouble()
let systemUnderTest = NetworkClient(cookiesProvider: cookiesProvider)
This will empower you to test the network client behaviour that depends on the
value of a cookie by simply setting it in your CookieProviderTestDouble
instance, as well as testing behaviour that results in a cookie being stored
by checking the values in the .cookies
property of the test double.
In your production code you will then have to extend NSHTTPCookieStorage
to
conform to CookieProvider
:
let apiURL: NSURL = ...
extension NSHTTPCookieStorage: CookieProvider {
func cookieWithName(name: String) -> NSHTTPCookie? {
return cookiesForURL(apiURL)?
.filter { $0.name == name }
.first
}
// A method called setCookie is already exposed by NSHTTPCookieStorage.
// Less work :)
func deleteCookieWithName(name: String) {
guard let cookie = cookieWithName(name) else {
return
}
deleteCookie(cookie)
}
}
Pros
This approach simplifies testability, and potentially makes your unit tests faster in case the third party code you are dealing with performs I/O operations.
Another great benefit you get by using protocols is that you can focus on the
function of the dependency rather that its implementation. For example when
using NSUserDefaults
to store the theme color preference of the user you can
use a protocol named ThemeColorProvider
. When reading code using the
ThemeColorProvider
it will be clear what this component does.
Cons
An important thing to be aware of is that since you are not using the real object in your tests, but rather a double that exposes the same interface, you expose yourself to the risk of bugs in the implementation of the real object.
For this reason I recommend using this approach only when depending on third party code that you cannot control, and that you are confident is well written and tested.
Leave the codebase better than you found it.