Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trouble getting OHHTTPStubs working with a custom Alamofire Session Manager. #252

Closed
6 of 9 tasks
minuscorp opened this issue May 25, 2017 · 22 comments
Closed
6 of 9 tasks

Comments

@minuscorp
Copy link

New Issue Checklist

Environment

  • version of OHHTTPStubs: 6.0.0
  • integration method you are using:
    • Cocoapods
    • Carthage
    • submodule
    • other
  • version of the tool you use: 1.2.1

Issue Description

I was trying to test my own CocoaPods library web services with OHHTTPStubs. All the service is built on top of Alamofire through a singleton which creates its own Alamofire.SessionManager created in the following way:

public var configuration: SessionManagerConfiguration = SessionManagerConfiguration.default {
        didSet(newValue) {
            let configuration = URLSession.shared.configuration
            if let token = UserDefaults.standard.string(forKey: newValue.userDefaultsTokenKey) {
                configuration.httpAdditionalHeaders = ["token": token]
                configuration.urlCache = nil
            }
            let delegate = Alamofire.SessionDelegate()
            let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
            self._sessionManager = Alamofire.SessionManager(session: session, delegate: delegate)!
            
            self.initializeObservables()
        }
    }

This is being correctly invoked in my beforeSuite{ } method when I create the singleton and set the configuration at the same time:

SessionManager.shared.configuration = SessionManagerConfiguration(withInvalidAuthenticationNotificationName: Notification.Name(rawValue: "testInvalidAuthenticationName"), invalidAuthenticationCode: 1500, userDefaultsTokenKey: "testTokenKey", baseURL: self.baseURL)

Where baseURL is just a random URL that I want to stub certain endpoints that satisfy my services, like this:

        describe("User Operations Spec") {
            
            it("Should retrieve an user") {
                
                OHHTTPStubs.setEnabled(true, for: SessionManager.shared.sessionManager.session.configuration)
                
                self.userDetailStub = stub(condition: isHost(self.baseURL)) {
                    _ in
                    let stubData = self.loadUserStub()
                    return OHHTTPStubsResponse(data: stubData, statusCode: 200, headers: nil)
                }
                
                var user: User!
                
                SessionManager.shared.service.user.user(.internet)
                    .observeOn(MainScheduler.instance)
                    .subscribe(onNext: {
                        _user in
                        user = _user
                    })
                    .disposed(by: self.disposeBag)
                
                expect(user).toEventuallyNot(beNil(), timeout: 5)
                expect(user.id_hash.int64Value) == 1446546480781
            }
        }

After running the test, the expectation did not get fulfilled so the tests fails, as shown in the lines below.

Complete output when you encounter the issue (if any)
/Users/GabrielVillarrubia/Documents/Private Pods/EbmCore/Example/Tests/User Operation Specs.swift:76: error: -[EbmCore_Tests.UserOperationSpecs User_Operations_Spec__Should_retrieve_an_user] : expected to eventually not be nil, got <nil>

fatal error: unexpectedly found nil while unwrapping an Optional value

There's something obvious that I'm skipping on this? There's no host app due to the fact that this is a CocoaPods framework's test suite.

Thank you very much and keep it up with the great work with this library!

@AliSoftware
Copy link
Owner

You have to create your first stub before creating your Alamofire.SessionManager.

Because Alamofire.SessionManager creates its internal URLSession from its own NSURLSessionConfiguration under the hood and if that session is created before OHHTTPStubs had any chance of injecting itself in the URLSessionConfiguration before the session is created using that configuration then it's too late to alter the session's configuration once it's created. OHHTTPStubs being lazily loaded of you don't call any method of OHHTTPStubs before creating the session it won't hack itself into the URLSessionConfigurations soon enough.

You can search closed issues in this repo, some people also had a similar setup as yours and made the mistake of creating the Alamofire.SessionManager or sending their first Alamofire request too soon before OHHTTPStubs got any chance to be loaded in memory by the system

@minuscorp
Copy link
Author

minuscorp commented May 25, 2017

So... I follows your instructions with some code rearrange:
Now, the stub is created at the spec initialization:

let userDetailStub: OHHTTPStubsDescriptor = {
        return stub(condition: isHost(UserOperationSpecs.baseURL)) {
            _ in
            let stubData = UserOperationSpecs.loadUserStub()
            return OHHTTPStubsResponse(data: stubData, statusCode: 200, headers: nil)
        }
    }()

and later on, on the test case:

// [...]
it("Should retrieve an user") {
    SessionManager.shared.configuration = SessionManagerConfiguration(withInvalidAuthenticationNotificationName: Notification.Name(rawValue: "testInvalidAuthenticationName"), invalidAuthenticationCode: 1500, userDefaultsTokenKey: "testTokenKey", baseURL: UserOperationSpecs.baseURL) 
    // <- Debugging checked that no Alamofire.Session is instantiated anywhere before this point
     OHHTTPStubs.setEnabled(true, for: SessionManager.shared.sessionManager.session.configuration)
// [...]

And the result is the same, the call is not being stubbed.

@AliSoftware
Copy link
Owner

But but but… when you write this:

OHHTTPStubs.setEnabled(true, for: SessionManager.shared.sessionManager.session.configuration)

You're accessing the configuration of an already created session, right?
So you're actually trying to modify a configuration that is already part of an existing session so after that session has been instantiated, right? 😉

@minuscorp
Copy link
Author

The session is being instantiated in the line you mention, far after the stub is being created.

@AliSoftware
Copy link
Owner

Btw maybe your issue has nothing to do with using Alamofire and that configuration stuff, I might have jumped at the wrong conclusion here.

I just saw that you wrote isHost(self.baseURL) but what does this baseURL actually contains? Does it really ONLY contains a host name, without http scheme or any /?

@AliSoftware
Copy link
Owner

So in the end it might just be a duplicate of #247 (comment) — which will be prevented by the preconditions added by #248 but are still only in master and not been released yet (new release coming soon to avoid more people falling in that trap)

@minuscorp
Copy link
Author

It's actually var baseURL = http://www.example.com I changed to isHost("example.com") without any success

@AliSoftware
Copy link
Owner

Could you try using an always-true condition instead? (stub({ _ in true }) { … })
And also adding some breakpoints to see if:

  • your request is properly being sent (like you didn't forget to call .resume() on the dataTask / call response() or responseJSON() on the AF request / call subscribe() on your Observable triggering the request
  • Your request is kept running and not being cancelled (like if your Observable's subscription isn't retained and ends too soon, being disposed quite immediately and the cancelling the request
  • The internal methods of OHHTTPStubs just even the ones checking if the protocol can handle the request, are called

@AliSoftware
Copy link
Owner

Finally, even though it might not be the direct source of your issue here, note again that — as the URLSession documentation says — once a session is created from an URLSessionConfiguration, that session's configuration is copied at init time and thus read-only: any change to the configuration after it has been used to build the session will not affect the session after it has been instantiated. So you're generally not supposed to access shared.session.configuration to modify it (that won't have any impact on the session) but rather supposed to create a new configuration, set it's properties, then construct the session from it and don't modify the configuration afterwards. But skimming through your code (from my phone so maybe I missed some tricks?) it seems you try at multiple occasions to access shared.session.configuration to modify it, am I right?
Even though idk if it's the source of your issue with stubs it feels to me that you should maybe start to fix that and make sure your test code works in the first place and you didn't overlook some survival case you have only because of that our because you're in tests (I've been bitten by that in the past so that's why I suggest that 😉) before adding stubs once the rest is guaranteed to work? I mean I'm sure you checked but better to rule everything out to be sure we're not trying to debug the wrong part ^^

@minuscorp
Copy link
Author

minuscorp commented May 25, 2017

Can you give me a clue of what internal methods should I breakpoint to debug if the request is at least being checked by the library? After that I check your next comment to try to solve what you're telling me, step by step... 😅

EDIT: I've checked all the other points in your first comment

@AliSoftware
Copy link
Owner

AliSoftware commented May 25, 2017

If you use a custom condition closure you can already put a breakpoint here and check some stuff, like this:

OHHTTPStubs.stub({ request in
  print(req) // breakpoint here and check some values
  print(req.url?.host)
  return true // return req.url?.host == "example.com"
}) { return OHHTTPStubsResponse () }

If that's not sufficient to debug, you could check if this method is getting called, as well as the startLoading method a few lines below.

@minuscorp
Copy link
Author

Neither of the methods are being called 😟

@AliSoftware
Copy link
Owner

Then this means that OHHTTPStubsProtocol hasn't been injected in you URLSessionConfiguration before the session has been created.

Could you check if OHHTTPStubsProtocol is present in the list of session.configuration.protocolClasses?

If not that means that the session has been created before OHHTTPStubs had time to auto-inject itself via swizzling into URLSessionConfiguration…

@minuscorp
Copy link
Author

(lldb) po SessionManager.shared.sessionManager.session.configuration.protocolClasses
▿ Optional<Array<AnyObject.Type>>
  ▿ some : 5 elements
    - 0 : _NSURLHTTPProtocol
    - 1 : _NSURLDataProtocol
    - 2 : _NSURLFTPProtocol
    - 3 : _NSURLFileProtocol
    - 4 : NSAboutURLProtocol

Tomorrow I'll dig into this...

@AliSoftware
Copy link
Owner

Ok then that's the issue.

You might wanna put a breakpoint in this method which is responsible for doing the swizzling — and thus auto-installing the OHHTTPStubsProtocol inside the default URLSessionConfiguration constructors — especially check:

  • That it's called only once and not twice (if it's called more that one that would strangely mean that the OHHTTPStubs library is linked & loaded twice in your target)
  • That it's called before Alamofire is creating it's own URLSession (I'll let you find the right place in Alamofire's source code to put a breakpoint then check which is called first)

@minuscorp
Copy link
Author

minuscorp commented May 25, 2017

The method you mention of OHHTTPStubs is getting called before this, this or this, which are, as far as I know, all the points where an Alamofire.SessionManager is getting created (and thus, the URLSession?)

EDIT: Also, I can ensure that your library method is being called firstly in the _XCTestMain even before any test in the suite.

@AliSoftware
Copy link
Owner

Mmmh very strange indeed. And the swizzling is only called once?

@minuscorp
Copy link
Author

Just called once. Checked.

@AliSoftware
Copy link
Owner

Very strange. Did you check that you added OHHTTPStubs to the right target? Well you said you didn't use hosted tests so your test target should be a standalone one be I can't see what's going on here…

Could you maybe create a sample project to reproduce the issue in an isolated way and share it there?

@minuscorp
Copy link
Author

Tomorrow I create a project where you can reproduce the issue and I put the link here.

P.D. it's 10 pm in Spain 😅

@minuscorp
Copy link
Author

After some tweaking here and there... I carefully read your comment talking about URLSession and how it handles its URLSessionConfiguration, so I modified my custom manager creation like this:

public var configuration: SessionManagerConfiguration = SessionManagerConfiguration.default {
        didSet(newValue) {
            let configuration = URLSessionConfiguration.default
            if let token = UserDefaults.standard.string(forKey: newValue.userDefaultsTokenKey) {
                configuration.httpAdditionalHeaders = ["token": token]
            }
            self._sessionManager = Alamofire.SessionManager(configuration: configuration)
            
            self.initializeObservables()
        }
    }

So now I'm simply using copies of URLSessionConfiguration.default which, as you said, should be being swizzled at test suite start. Now is correctly running all the tests independently of the order or the time of the stub addition, (at least those whose are added before the Alamofire.SessionManager is instantiated for the first time.

Thank you very much, I would love to see this more explicitly explained for beginner users and visible in the README.md as feedback.

@AliSoftware
Copy link
Owner

Cool glad you managed to solve it. I'd love a PR to improve the README in that matter or even a dedicated wiki page maybe (the wiki is open so you should be able to contribute freely)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants