Blog Update 20th August 2014: This initial post was tested using Xcode6 beta4, with Xcode beta5 Swizzling was broken. From release notes "Dynamic dispatch can now call overrides of methods and properties introduced in class extensions, fixing a regression introduced in Xcode 6 beta 5. (17985819)!"
See stackoverflow question for more details
Happy beta6 Swifting!
Last weeks, I was delving into http and Swift with my friend Christos. To write robust code, we need unit tests of course and to isolate testing layer: you need a HTTP stub. See stackoverflow question for more details
Happy beta6 Swifting!
When we worked with Objective-C, we used to mock http calls with OHTTPStubs. So we thought we could carry on using it with our brand new Swift code. After all, Swift and Objective-C are interoperable. However, there are subtle differences with iOS8 and when trying to use OHTTPSubs obj-c lib with Swift code, we ran into this issue (merged now!).
But when you delve into some neat code, then the urge of writing your own is showing up... It wasn't long before my friend Christos came up with AGURLSessionStubs a http stub written entirely in Swift.
With this blog post, I'd like to share with you the inside of stubbing http library. Let's dig under the hood to see how http mock are implemented in Objective-C and how we can apply the same concepts (with some restrictions) in Swift.
Implementing NSURLProtocol
As Mattt Thompson linked this excellent blog post on how to stub class method by registering a customized NSURLProtocol. All you have to do is implement your own mocked NSURLProtocol class. Let's see how to do it in Swift:class StubURLProtocol: NSURLProtocol { let stubDescr: StubDescriptor override class func canInitWithRequest(request: NSURLRequest!) -> Bool { return StubsManager.sharedManager.firstStubPassingTestForRequest(request) != nil } ... override func startLoading() { let request: NSURLRequest = self.request let client: NSURLProtocolClient = self.client; let responseStub: StubResponse = self.stubDescr.responseBlock(request) let urlResponse = NSHTTPURLResponse(URL: request.URL, statusCode: responseStub.statusCode, HTTPVersion: "HTTP/1.1", headerFields: responseStub.headers) client.URLProtocol(self, didReceiveResponse: urlResponse, cacheStoragePolicy: NSURLCacheStoragePolicy.NotAllowed) client.URLProtocol(self, didLoadData: responseStub.data) client.URLProtocolDidFinishLoading(self) } }And now we're left with registering. NSURLSession is initialised by passing it a configuration with a class method:
let config = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: config)How can I switch between stub and real implementation? One way of doing it would be to inherit and implement NSURLSessionConfiguration, another way would be to swap method implementation of NSURLSessionConfiguration.defaultSessionConfiguration() with mock swizzle_defaultSessionConfiguration() which contains code to register our mocked NSURLProtocol class.
Method swizzling
The runtime dynamic method replacement technique known as method swizzling uses Objective-C runtime meta-programming to swap methods at runtime. Since we have interoperability Swift/Objective-C, if the object you want to mock inherits NSObject (in our scenario with NSURLSessionConfiguration, this is the case), you can use runtime meta-programming.Here is how to swap 2 methods contents.
private class func swizzleFromSelector(selector: String!, toSelector: String!, forClass:AnyClass!) { var originalMethod = class_getClassMethod(forClass, Selector.convertFromStringLiteral(selector)) var swizzledMethod = class_getClassMethod(forClass, Selector.convertFromStringLiteral(toSelector)) method_exchangeImplementations(originalMethod, swizzledMethod) }In this example we simply swap the content of method from defaultSessionConfiguration to swizzle_defaultConfiguration. You can simply defined swizzle_defaultSessionConfiguration as an extension of NSURLSessionConfiguration:
extension NSURLSessionConfiguration { class func swizzle_defaultSessionConfiguration() -> NSURLSessionConfiguration! { // as we've swap method, calling swizzled one here will call original one let config = swizzle_defaultSessionConfiguration() var result = [AnyObject]() for proto in config.protocolClasses { result += proto } // add our stub result.insert(StubURLProtocol.classForCoder(), atIndex: 0) config.protocolClasses = result return config } }The trick is when you call NSURLSessionConfiguration.defaultSessionConfiguration() you actually call NSURLSessionConfiguration.swizzle_defaultSessionConfiguration() and vice versa. So the first call line 5 is not a recursive call but the call to the original method! From a user point of view, here is the way to write a test:
func testGETWithoutParametersStub() { func http_200(request: NSURLRequest!, params:[String: String]?) -> StubResponse { var data: NSData if (params) { data = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: nil) } else { data = NSData.data() } return StubResponse(data:data, statusCode: 200, headers: ["Content-Type" : "text/json"]) } func http_200_response(request: NSURLRequest!) -> StubResponse { return http_200(request, params: ["key1":"value1"]) } // set up http stub StubsManager.stubRequestsPassingTest({ (request: NSURLRequest!) -> Bool in return true }, withStubResponse:( http_200_response )) // async test expectation let getExpectation = expectationWithDescription("Retrieve data with GET without parameters"); var url = "http://whatever.com" var http = AGSessionImpl(url: url, sessionConfig: NSURLSessionConfiguration.defaultSessionConfiguration()) http.GET(nil, success: {(response: AnyObject?) -> Void in if response { XCTAssertTrue(response!["key1"] as NSString == "value1") getExpectation.fulfill() } }, failure: {(error: NSError) -> Void in XCTAssertTrue(false, "should have retrieved jokes") getExpectation.fulfill() }) waitForExpectationsWithTimeout(10, handler: nil) }1. line16-19: you need to instantiate your stub and tell runtime: go swizzling from now on when you call NSURLSessionConfiguration.defaultSessionConfiguration() I really mean NSURLSessionConfiguration.swizzle_defaultSessionConfiguration() because this one instantiate my mock NSURLProtocol and put it before all the other protocols so i can stub http calls now.
2. line 2-14: stub your response
3. line 25-33: do you work with NSURLSession as usual
4. line 27-28: assert
5. in teardown method don't forget to remove your stub with StubsManager.removeAllStubs()
Swizzling in Swift: is it needed?
Yes, some would says.Matt says: this may not actually be necessary in Swift
Although others ways exist (missing class variable in Swift though) I couldn't find an easy way to switch two methods content as shown in the previous example.
Want to know more about AGURLSessionStubs
Go and check AGURLSessionStubs, see how to use it with push registration lib or aerogear-ios-http lib.Special thanks to my friend Christos who shared with me most of the articles linked in the post and who is the creator of AGURLSessionStubs where code snippets comes from.
Happy Swifting!
Feedback welcome!
Tweet