AliSoftware / OHHTTPStubs

Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!
MIT License
5.03k stars 601 forks source link

Not compatable with SWHttpTrafficRecorder #232

Closed Amindv1 closed 7 years ago

Amindv1 commented 7 years ago

I wanted to point out that this pod isn't compatible with the SWHttpTrafficRecorder as it points out on the readme. Both this pod and that pod try to register their own versions of NSURLProtocol which means one of them is going to lose, so either you don't record the file or you don't stub the response. I spent a good chunk of time trying to figure out why they weren't working together and thought this should be noted on your readme, as it makes it look like it's been tested and it should work when in reality it doesn't.

AliSoftware commented 7 years ago

Thanks for the issue.

It has been tested… but separately, as you usually don't both stub and record what you stub, but rather record first and stub later.

But you're right in that it should be mentioned in the README that you should not use both at the same time but rather one after the other, so the README should be more clear about that.

Amindv1 commented 7 years ago

So the reason I have a two in one is that I try to automate everything. I attempt to record, if the file name for the request doesn't exist then I do record the request, otherwise if it does exist I stub the request based off of the file. This requires me to register both.

AliSoftware commented 7 years ago

I see.

I think you should still be able to register OHHTTPStubs manually (I don't remember of SWHttpTrafficRecorder offers an API to register the NSURLProtocol manually but OHHTTPStubs has setEnabled: and setEnabled: forSession: methods that should allow you to add OHHTTPStubs' internal NSURLProtocol manually even after SWHttpTrafficRecorder did set itself up and added is own, making them both intercept the requests (I indeed haven't tested this use case of supporting them both at the same time but it's worth a try as it should theoretically work if you do it this way)

Amindv1 commented 7 years ago

I thought that only one NSURLProtocol can be registered at a time? I don't think I understand your proposed solution. Can you show me what you mean? This would save me a lot of heartache, I was considering merging the two pods into one so I can merge their protocols and use them at the same time.

AliSoftware commented 7 years ago

No you can register as many as you want. Let me find a Mac in a minute so I can give you an example

AliSoftware commented 7 years ago

Ok, so, basically my proposed solution is for you to call OHHTTPStubs.sharedInstance.setEnabled(true, forSessionConfiguration: sessionConfig) (assuming you use a custom NSURLSession build from an NSURLSessionConfiguration, and not NSURLConnection nor the shared NSURLSession) before creating your NSURLSession with it.

You can find this method's implementation here. As you can see, this method doesn't replace the supported NSURLProtocols of an NSURLSession, but instead just inserts OHHTTPStubs's own NSURLProtocol at the top of the sessionConfig.protocolClasses array.

This insertion is automatically done on the two NSURLSessionConfiguration's defaultSessionConfiguration and ephemeralSessionConfiguration presets (thanks to some swizzling).

If you use SWHttpTrafficRecorder, it also inserts its own NSURLProtocol in this array (see here). But for this to work you'll have to:

So, now that I've had time to re-read the implementation of both pods, I don't see why this wouldn't work:

  1. Ensure that you have the pod 'OHHTTPStubs' pod in your Podfile. If you only have pod 'OHHTTPStubs/Swift' for example, this subspec doesn't contain the NSURLSession-supporting subspec by default (this might change in the future, but for now the Swift subspec only contains the Swift helpers, not the other subspecs. See the dedicated section in the README about this
  2. Use this kind of code (I wrote it in Swift but should work the same in ObjC):
import Foundation

// Start with the default configuration
let config = URLSessionConfiguration.default

// If you indeed have the `OHHTTPStubs/NSURLSession` subspec listed in your `Podfile.lock`
// then OHHTTPStubs should have automatically  injected its own `NSURLProtocol` at this point
// You can check this using some NSLog:
NSLog("protocol classes after OHHTTPStubs: \(config.protocolClasses)") // this should contain `OHHTTPStubsProtocol` among others

do {
  // not sure about the translation of SWHttpTrafficRecorder API in Swift here, I'll let you adapt
  let started = try SWHttpTrafficRecorder.sharedRecorder().startRecording(path:nil, forSessionConfiguration:config)
  // At this point, SWHttpTrafficRecord should itself have added its own NSURLProtocol to the
  // protocolClasses array, intercepting the traffic first, BEFORE anybody else (including OHHTTPStubs)
  NSLog("Started: \(started)")
  NSLog("protocolClasses after SWHTR: \(config.protocolClasses)")
} catch {
  NSLog("Error while trying to start SWHttpTrafficRecorder")
}

// At this point, SWHttpTrafficRecorder is listed first in the protocolClasses array
// then OHHTTPStubs is next in line. This means that SWHTR will intercept requests first, then
// OHHTTPStubs will be given the ability to stub them next, then if the request passed thru both
// the request will finally hit the real world.

// If you want to make `OHHTTPStubs` intercept the requests BEFORE SWHttpTrafficRecorder
// Then you have to ask `OHHTTPStubs` to remove itself then re-insert itself at index 0
OHHTTPStubs.setEnabled(false, forSessionConfiguration: config) // remove first
OHHTTPStubs.setEnabled(true, forSessionConfiguration: config) // then reinsert, at index 0

// And ONLY then, create your NSURLSession, because if you create it before
// setting up the NSURLSessionConfiguration, changes in the configuration won't do anything
let session = URLSession(configuration: config)
let task = session.dataTask(with: yourURLHere)
task.resume()
Amindv1 commented 7 years ago

What's the difference between adding the protocol like so:

[NSURLProtocol registerClass:OHHTTPStubsProtocol.class];

and like so:

NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; Class protoCls = OHHTTPStubsProtocol.class; if (enable && ![urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses insertObject:protoCls atIndex:0]; } else if (!enable && [urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses removeObject:protoCls]; } sessionConfig.protocolClasses = urlProtocolClasses; ?

If I register the class for NSURLProtocol does it register it for the default protocol, so any call to URLSessionConfiguration.default after adding a protocol will include the new protocol?

AliSoftware commented 7 years ago

The difference is that one applies to global sessions/connections, the other to sessions built with a given session configuration.

For more info, see:

For in-depth understanding, you can also find detailed information in Apple's API documentation on NSURLSession / NSURLSessionConfiguration and the URL Loading System Programming Guide

Amindv1 commented 7 years ago

Alright maybe I'm confused here but something doesn't seem right. I was assuming that the setEnabled method is what was registering the class under the default session configuration, which is what AFNetworking was using. But after some testing I don't know if this is the case, how are you making it so that you stub AFNetworking requests? The reason I don't think it's the registerClass function is because SWHttpTrafficRecorder uses that to register its own protocol class but when I do:

print("before: ", URLSessionConfiguration.default.protocolClasses)
print("did register AFRecordingProtocol.self: ", URLProtocol.registerClass(AFRecordingProtocol.self))    // This is my custom recording protocol looks exactly like SW's    
print("after: ", URLSessionConfiguration.default.protocolClasses)

the before and after are the same:

before: Optional([OHHTTPStubsProtocol, _NSURLHTTPProtocol, _NSURLDataProtocol, _NSURLFTPProtocol, _NSURLFileProtocol, NSAboutURLProtocol]) did register AFRecordingProtocol.self: true after: Optional([OHHTTPStubsProtocol, _NSURLHTTPProtocol, _NSURLDataProtocol, _NSURLFTPProtocol, _NSURLFileProtocol, NSAboutURLProtocol])

As you can see, OHHTTPStubsProtocol is being registered but my custom protocol isn't listed. How is OHHTTPStubs making it so that the class is being registered with AFNetworking? You mentioned method swizzling but can you point me to where to start looking to see how this is happening? The session manager I'm using is an AFHTTPSessionManager and I pass it the default configuration:

[NSURLSessionConfiguration defaultConfiguration];

I make sure to setup the session configuration after I've added the protocol classes, so that isn't the issue either.

I'm using swift and objc interchangeably because my project has both and the code snippets come from different files.

Amindv1 commented 7 years ago

Alright so I took a look at OHHTTPStubs+NSURLSessionConfiguration and see where the method swizzling is happening. Now I understand my error. I was thinking that the URLProtocol RegisterClass was what was adding OHHTTPStubs to the session configuration but this isn't the case, it's being 'swizzled' in. Thanks for pointing that out in your comment, it took me a couple of reads through to understand exactly what you meant and I also had to read through the implementation and on method swizzling. Here's the post I read if anyone else that stumbles upon this is as confused as I was:

http://nshipster.com/method-swizzling/

AliSoftware commented 7 years ago

Yup that's it.

Again, it's explained explicitly here ("Automatic Loading" paragraph) in the README as pointed out before.

Amindv1 commented 7 years ago

Yea I read through that a bunch of times actually but since I'm a bit of a noob I didn't understand exactly what you meant by swizzling. I thought that the registerclass was what was swizzling the method because I didn't understand the concept of swizzling. I think all the new terminology threw me off too so I was confused when reading through the readme.

Again I really appreciate the help!

AliSoftware commented 7 years ago

Ah right. Maybe we should put that NSHipster's link next to the "swizzling" mention in the README then.

Amindv1 commented 7 years ago

Maybe that and mentioning where you're swizzling the sessions, what really did it for me is actually seeing where you guys swizzled the config.