An Easy Way To Stub NSURLSession
If you follow this blog for some time you might have realized that one
of my favorite testing problems is the stubbing of NSURLSession
.
In case you are not familiar with the terms, stubbing means to fake the
answer of a method. In the case of NSURLSession
this means that we fake
the web API response. This has several advantages. For example:
- We don’t need a working web API to developer the network requests of our app.
- The response is instantaneous. This results in a faster feedback loop.
- The tests can run on a computer that has no internet connection.
Normally the stubbing of NSURLSession
requests is done using
NSURLProtocol
. Libs doing that are for example
OHHTTPStubs and
Mockingjay. The advantage of
using NSURLProtocol
is, that the stubbing also works when using libs as
Alamofire for the network
requests. This works great but for me this is to much code. I would have
to study and understand that code to get the desired confidence im my
tests.
An easy solution
I use NSURLSession
for my network requests. Here is how I stub my
requests.
To make it easier, I have written a mock class for NSURLSession
and a
protocol. All that together looks like this:
import Foundation
public protocol DHURLSession {
func dataTaskWithURL(url: NSURL,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask
func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask
}
extension NSURLSession: DHURLSession { }
public final class URLSessionMock : DHURLSession {
var url: NSURL?
var request: NSURLRequest?
private let dataTaskMock: URLSessionDataTaskMock
public init(data: NSData?, response: NSURLResponse?, error: NSError?) {
dataTaskMock = URLSessionDataTaskMock()
dataTaskMock.taskResponse = (data, response, error)
}
public func dataTaskWithURL(url: NSURL,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask {
self.url = url
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}
public func dataTaskWithRequest(request: NSURLRequest,
completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask {
self.request = request
self.dataTaskMock.completionHandler = completionHandler
return self.dataTaskMock
}
final private class URLSessionDataTaskMock : NSURLSessionDataTask {
typealias CompletionHandler = (NSData!, NSURLResponse!, NSError!) -> Void
var completionHandler: CompletionHandler?
var taskResponse: (NSData?, NSURLResponse?, NSError?)?
override func resume() {
completionHandler?(taskResponse?.0, taskResponse?.1, taskResponse?.2)
}
}
}
So, the complete helper code I need to write my stubs is 47 lines of code. And all that code is easy to understand. No swizzling, not complicated methods. Nice!
The usage
To be able to use the NSURLSession
mock in the test, we need a way to
inject the dependency into the code. One possibility is the usage of a
lazy property:
lazy var session: DHURLSession = NSURLSession.sharedSession()
Then an example test would look like this:
func testFetchingProfile_ReturnsPopulatedUser() {
// Arrage
let responseString = "{\"login\": \"dasdom\", \"id\": 1234567}"
let responseData = responseString.dataUsingEncoding(NSUTF8StringEncoding)!
let sessionMock = URLSessionMock(data: responseData, response: nil, error: nil)
let apiClient = APIClient()
apiClient.session = sessionMock
// Act
apiClient.fetchProfileWithName("dasdom")
// Assert
let user = apiClient.user
let expectedUser = User(name: "dasdom", id: 1234567)
XCTAssertEqual(user, expectedUser)
}
I really like this approach because I can understand the mocking within
a few minutes by scanning over 50 lines of code. There is no
NSURLProtocol
and not swizzling involved.
The NSURLSession
mock is on
github and it’s also
available via CocoaPods.
Let me know what you think.