Another Approach To Stubbing NSURLSession With Dependency Injection
Recently I watched a video by Jon Reid. Jon Reid explains how he uses unit tests to test networking code. This video was an eye opener for me. If you haven't watched it yet, do it now.
Here is how his approach looks like in Swift using NSURLSession:
First we need a property to inject the mock url session in the test:
public class Foo {
public var session: URLSession = NSURLSession.sharedSession()
}
Note the type of session . It is of type URLSession. URLSession is a protocol which declares one method:
public protocol URLSession {
func dataTaskWithURL(url: NSURL, completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask
}
To make the compiler happy with the above definition we need to tell it that NSURLSession implements the URLSession protocol:
extension NSURLSession: URLSession {
}
The networking code in this example is quite easy. It just fetches a user profile from App.net:
let urlString = "https://api.app.net/users/@(username)"
let task = session.dataTaskWithURL(NSURL(string: urlString)!, completionHandler: { (data, response, error) -> Void in
let rawDictionary = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as! [String:AnyObject]
self.user = self.userFromDictionay(rawDictionary)
})
task.resume()
The method userFromDictionay(_:) extracts and unwraps the elements which are needed to create a user and returns a user or nil if not all elements could be extracted:
func userFromDictionay(dictionary: [String:AnyObject]) -> User? {
if let rawUser = dictionary["data"] as? [String:AnyObject],
username = rawUser["username"] as? String,
name = rawUser["name"] as? String,
counts = rawUser["counts"] as? [String:Int],
followers = counts["followers"],
following = counts["following"],
posts = counts["posts"] {
return User(username: username, name: name, numberOfPosts: posts, followers: followers, following: following)
}
return nil
}
To store the user, we also need a user property:
public var user: User?
The user looks like this:
import Foundation
public struct User {
public let username: String
public let name: String
public let numberOfPosts: Int
public let followers: Int
public let following: Int
public init(username: String, name: String, numberOfPosts: Int, followers: Int, following: Int) {
self.username = username
self.name = name
self.numberOfPosts = numberOfPosts
self.followers = followers
self.following = following
}
}
The initializer is needed because we need to initialize a user in the tests and it seems that the generated initializer is declared as internal.
That's all for the networking code.
Let's switch to the test code. The mock url session looks like this:
class MockURLSession: URLSession {
typealias Handler = (NSData!, NSURLResponse!, NSError!) -> Void
var completionHandler: Handler?
var url: NSURL?
func dataTaskWithURL(url: NSURL, completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask {
self.url = url
self.completionHandler = completionHandler
return NSURLSessionDataTask()
}
}
Note that the mock url session stores the completion handler in a property.
The test looks like this:
func testFetchingOfUserSetUserProperty() {
// Arrange
let mockURLSession = MockURLSession()
secondViewController.session = mockURLSession
// Act
secondViewController.fetchUserWithUsername("dasdom")
let userDictionary = ["data": ["username": "dasdom", "name": "Dominik", "counts": ["followers": 11, "following": 22, "posts": 33]]] as NSDictionary
let data = NSJSONSerialization.dataWithJSONObject(userDictionary, options: nil, error: nil)
mockURLSession.completionHandler!(data!, nil, nil)
// Assert
let testUser = User(username: "dasdom", name: "Dominik", numberOfPosts: 33, followers: 11, following: 22)
XCTAssertTrue(secondViewController.user! == testUser, "should be equal")
}
First we inject the mock url session. Then we fetch a user and call the caught completionHandler with a test JSON. Finally we assert that the fetched user is as we expected.
To make this work we also need to override the == operator:
public func ==(left: User, right: User) -> Bool {
if left.username != right.username {
return false
}
if left.name != right.name {
return false
}
if left.numberOfPosts != right.numberOfPosts {
return false
}
if left.followers != right.followers {
return false
}
if left.following != right.following {
return false
}
return true
}
Keep in mind that the response of a NSURLDataTask is delivered on a background thread. So you can't use this approach to check if a label got updated as a result of the response. But in this case you would test two things at the same time. You shouldn't do this anyway.
If you have any comments or questions about this, ping be on App.net or Twitter.