Stubbing NSURLSession With Dependency Injection
I think I found a nice way to stub NSURLSession using dependency injection.
Let's say you have an app that should fetch the info of a user from App.net and create a user object/struct from that info. To be able to unit test that feature you want to stub the HTTP request in the test.
You could use Mockingjay or you could write your own NSURLProtocol. But with Swift 1.2 there is an even simpler approach.
In the test you can inject the mock class to be used instead of NSURLSession.
Here is the view controller with the method to fetch the user data:
import UIKit public class SwiftViewController: UIViewController { public var MySession = NSURLSession.self public var user: User? public func loadUser() { let url = NSURL(string: "https://api.app.net/users/@dasdom") let session = MySession.sharedSession() let task = session.dataTaskWithURL(url!, completionHandler: { (data, response, error) -> Void in let dataDict = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as! [String:AnyObject] let userDict = dataDict["data"] as! [String:AnyObject] self.user = User(username: userDict["username"] as! String, name: userDict["name"] as! String, id: userDict["id"] as! String) }) task.resume() } }
This code is straight forward. The only piece that is noteworthy it the first property:
public var MySession = NSURLSession.self
MySession is a NSURLSession type.
Let's write a mock for that class. You will have to mock NSURLSession and NSURLSessionDataTask. Here is the smallest implementation that can stub dataTaskWithURL(_:,completionHandler:):
class MockSession: NSURLSession { var completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)? static var mockResponse: (data: NSData?, urlResponse: NSURLResponse?, error: NSError?) = (data: nil, urlResponse: nil, error: nil) override class func sharedSession() -> NSURLSession { return MockSession() } override func dataTaskWithURL(url: NSURL, completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask { self.completionHandler = completionHandler return MockTask(response: MockSession.mockResponse, completionHandler: completionHandler) } class MockTask: NSURLSessionDataTask { typealias Response = (data: NSData?, urlResponse: NSURLResponse?, error: NSError?) var mockResponse: Response let completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)? init(response: Response, completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) { self.mockResponse = response self.completionHandler = completionHandler } override func resume() { completionHandler!(mockResponse.data, mockResponse.urlResponse, mockResponse.error) } } }
The mock session class has a static property (which will be used as a class property here) to store the mock response and a property to keep a reference of the passed in completion handler when dataTaskWithURL... is called. It overrides the class method sharedSession() and dataTaskWithURL(_:,completionHandler).
The mock task class is defined within the mock session class. It has two properties, dataTaskWithURL(_:,completionHandler) and completionHandler and it overrides resume() to call the completion handler with the test response instead.
The test then looks like:
func testExample() { let jsonData = NSJSONSerialization.dataWithJSONObject(["meta": ["code": 200], "data": ["username": "dasdom", "name": "Dpminik Hauser", "id": "1472"]], options: nil, error: nil) let urlResponse = NSHTTPURLResponse(URL: NSURL(string: "https://api.app.net/posts/stream/global")!, statusCode: 200, HTTPVersion: nil, headerFields: nil) MockSession.mockResponse = (jsonData, urlResponse: urlResponse, error: nil) viewController.MySession = MockSession.self XCTAssertTrue(viewController.user == nil, "") viewController.loadUser() XCTAssertEqual(viewController.user!.username, "dasdom", "") }
Note the dependency injection on the line:
viewController.MySession = MockSession.self
That's it. Light and easy to read. HTTP stubbing done within minutes.
I would love to hear your feedback to this stubbing method.