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

OHHTTPStubs not mocking when Alamofire request made in AppDelegate #126

Closed
iwllyu opened this issue Oct 1, 2015 · 25 comments
Closed

OHHTTPStubs not mocking when Alamofire request made in AppDelegate #126

iwllyu opened this issue Oct 1, 2015 · 25 comments

Comments

@iwllyu
Copy link

iwllyu commented Oct 1, 2015

Not sure if I should post this issue here or over at Alamofire

I have a unit test that stubs httpbin.org, and then it makes an Alamofire request. The request is stubbed as expected.

If I then add an Alamofire request to AppDelegate, the stub in the unit test stops mocking and instead the Alamofire request in the unit test calls the actual service.

I have a sample here https://github.com/iwllyu/test-ohhttpstubs

  • Swift 2.0
  • Xcode 7.0.1 (7A1001)
  • OHHTTPStubs 4.3.0 with swift support
  • Alamofire 2.0.2
  • Quick/Nimble
@AliSoftware
Copy link
Owner

Did you read my dedicated troubleshooting page in my wiki? Especially this one explaining the difference between Hosted Tests (the app bundle is launched and the test bundle is injected into it ==> in that case you need to install the stubs in the app bundle) vs Standalone Tests (the test bundle is launched standalone, without the app bundle even needed to be build, and you install your stub in the tests bundle).

If you want the stubs to be able to intercept any network request, you need to install them in the same bundle that is launched during test, which is the app bundle for hosted test or the test bundle for non-hosted tests.

@iwllyu
Copy link
Author

iwllyu commented Oct 1, 2015

Thanks for the response! I didn't think it was a test set-up error because the tests were working before we updated to Swift 2.0 but perhaps it was just coincidental with some other changes we were making

@AliSoftware
Copy link
Owner

Yeah, hosted tests are now the default in Xcode, so maybe migrating to Xcode 7 force-changed that setup?

@iwllyu
Copy link
Author

iwllyu commented Oct 1, 2015

Perhaps. I do have a question after reading the wiki.

It says not to use OHHTTPStubs in the app code (to mock TBD services) - but in the sample code it's only in the test target. So if I understand correctly I do not need to "...avoid linking OHHTTPStubs to the test target so that it is only loaded once" since it's only loaded to the test target when the application loads?

@AliSoftware
Copy link
Owner

I'm not sure to follow.

It says not to use OHHTTPStubs in the app code (to mock TBD services)

On the contrary, you could use OHHTTPStubs in your app code when you need to mock TBD services

but in the sample code it's only in the test target

What is? iirc, the sample code displays some UISwitches in the main view to toggle the stubs, and those stubs are installed in the application code, not in the tests. Actually the example projects don't contain any test at all, as the Unit Testing of the library is done in the dedicated project building the library/framework in Sources/, not in the sample projects in Examples/

@iwllyu
Copy link
Author

iwllyu commented Oct 1, 2015

On the contrary, you could use OHHTTPStubs...

Sorry, I mean the wiki states don't use OHHTTPStubs in app and in unit tests when using hosted tests

By sample code I'm referring to my sample of the "bug" I'm experiencing https://github.com/iwllyu/test-ohhttpstubs

@AliSoftware
Copy link
Owner

I just opened your sample project and that confirms what I stated earlier.

You're using hosted tests apparently, so this means that Xcode will launch your app bundle (and call application(_:didFinishLaunchingWithOptions:)), then inject your test bundle.
This means that all network requests done by your app code, like the one in your AppDelegate, will use OHHTTPStubs registered in that app bundle, not the ones registered in the test bundle.

@iwllyu
Copy link
Author

iwllyu commented Oct 2, 2015

Thanks for taking a look.

Okay here is where I am confused

per the wiki

If you linked OHHTTPStubs with both the app and the test bundle, there will be two independent instances of OHHTTPStubs loaded at runtime.

OHHTTPStubs is not linked in the app bundle, only the test bundle

Every code loaded by the Application Bundle (like code from your model classes) will use the instance of OHHTTPStubs loaded by the application

It is fine (for now, anyways) if the Alamofire request in the App calls makes an actual request

Every code loaded by the Test Bundle (like code in your test suite) will use the instance of OHHTTPStubs loaded by the test bundle

This is not happening, the test Alamofire request is calling the actual service even though there is an OHHTTPStub registered before the request. Removing the Alamofire request in the app resolves this.

Let me know if I misunderstood something

@AliSoftware
Copy link
Owner

Removing the Alamofire request in the app resolves this.
Ah, I didn't get that on my first look on your sample, indeed if just commenting the call to Alamofire.request in your AppDelegate changes the behavior, I see now how weird it feels!

So, I finally dug a bit deeper in your sample code, and this was indeed a tricky case, which I believe is linked with 3 factors:

  • the way Alamofire initialize its internal NSURLSession the first time Alamofire.request is called,
  • the fact that you're using Hosted Tests,
  • and the way OHHTTPStubs auto-loads itself.

All that explains why, when you're doing a request in your App code, the stubs are not properly setup in time.

Here is what happens in practice:

  1. You start running the tests.
  2. Because they are Hosted Tests, the first thing Xcode does is to launch your application bundle. This means that your application(_:didFinishLaunchingWithOptions:) gets executed right away, before any test case start, even before the test bundle is loaded itself
  3. There, your AppDelegate calls your Alamofire.request(…) code, which is the first call to Alamofire so far
  4. Alamofire.request internally calls its static Manager.sharedInstance instance, which is a singleton (static let is how to do singletons in Swift) which creates and store an NSURLSessionConfiguration the first time that sharedInstance property is accessed
  5. That request made by the app hits the real world, as we're still in the process of launching your App and the Tests bundle still hasn't been loaded by Xcode at that point, and neither does OHHTTPStubs (and no stub has even been setup yet)
  6. Then only once the app has finished launching, the test bundle is finally loaded and injected into the app
  7. This is where OHHTTPStubs is loaded (because you've linked it to your unit tests target so it gets loaded only once your Unit Tests bundle is loaded and injected into the app by Xcode)
  8. This is when OHHTTPStubs swizzles NSURLSessionConfiguration so it can inject itself into newly created instances and thus intercept every outgoing requests (see here in OHHTTPStubs' code)
  9. But Alamofire has already created its internal NSURLSessionConfiguration at that point (see 4), on which stubs were not activated yet. So it's too late for him, it is already using an NSURLSession that got initialized with an NSURLSessionConfiguration without stubs installed.

Some ideas to fix your edge case:

  • Switch to non-hosted tests, so that your App doesn't get launched before your test bundle is loaded
  • Keep hosted tests, but add OHHTTPStubs to your app bundle instead of your test bundle, so that it's loaded as soon as the app is launched when running your tests
  • Hack your AppDelegate code somehow, so that when launched from the tests, it doesn't make any call to Alamofire.request, and that the first request made by Alamofire in the app or in the test (thus triggering its call to Manager.sharedInstance that creates its internal NSURLSessionConfiguration) is only done after the test bundle has been loaded and injected into your app bundle.

The fact that the App bundle is loaded and your app is started (and the AppDelegate methods executed) before the test bundle (and OHHTTPStubs) is even loaded and injected is the core of your problem. One of the 3 proposed solutions above would allow you to solve or workaround that.

@iwllyu
Copy link
Author

iwllyu commented Oct 2, 2015

I really appreciate the time you spent looking into this.

I've made it so Alamofire requests aren't made during startup (there was a view controller being initialized that made the API call, I delayed the request until the viewWillAppear) - although we could also switch to non-hosted since we aren't doing any UI testing.

Thanks again for all the help

@iwllyu iwllyu closed this as completed Oct 2, 2015
@rcdilorenzo
Copy link

@AliSoftware: Your last comment here was very helpful for me as well. Thanks!

@AliSoftware
Copy link
Owner

@rcdilorenzo thanks for the feedback, glad it helped.

It could maybe be a nice addition to make a wiki page or article about that; I'd be glad to add a link to it in the README if it can help others.

@adamwaite
Copy link

God bless you @AliSoftware

@igorbelo
Copy link

hello @AliSoftware I'm swift/xcode beginner and have to write some tests for my company's app. I'm sorry if it's a too silly question (and it's not related to OHHTTPStubs directly) but how do I do the following?

... add OHHTTPStubs to your app bundle instead of your test bundle, so that it's loaded as soon as the app is launched when running your tests

@AliSoftware
Copy link
Owner

@igorbelo If you integrated OHHTTPStubs using CocoaPods, simply move your pod 'OHHTTPStubs' line in your Podfile from the target 'YourAppTests' do … end block into target 'YourApp' do … end block.

If you integrated using Carthage, just change the OHHTTPStubs.framework target membership in the right panel one you have selected the framework in Xcode, to uncheck the "YourAppTests" checkbox and check "YourApp" checkbox instead.

@igorbelo
Copy link

igorbelo commented Jul 23, 2016

@AliSoftware even doing that I'm getting a hit in my API server...don't know if it's due an outdated version of CocoaPods (0.39.0) we are using. Digging a little bit more to see if worth to open an issue for that.

UPDATE: even upgrading CocoaPods to last version and scoping the libs into targets I couldn't manage it to work.

Podfile

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '8.0'
use_frameworks!

def shared_pods
    # Forced using Github as source. Specifying only the Pod Name was fetching an older version
    pod 'Pluralize.swift', git: 'https://github.com/joshualat/Pluralize.swift.git'

    # Replacement for UIPageControl
    pod 'SMPageControl'

    # SwiftyJson
    pod 'SwiftyJSON', :git => 'https://github.com/SwiftyJSON/SwiftyJSON.git'

    # Alamofire https://github.com/Alamofire/Alamofire
    pod 'Alamofire', '3.2.0'

    # SwiftValidator https://github.com/jpotts18/SwiftValidator
    pod 'SwiftValidator', '~> 3.0.1'

    # https://github.com/honghaoz/AutoKeyboardScrollView
    pod "AutoKeyboardScrollView", '~> 1.4'

    # HanekeSwift https://github.com/Haneke/HanekeSwift
    pod 'HanekeSwift'

    # https://github.com/gilesvangruisen/Swift-YouTube-Player
    pod 'YouTubePlayer'

    # https://github.com/scalessec/Toast-Swift
    pod 'Toast-Swift', '~> 1.3.0'

    # https://github.com/oarrabi/OAStackView
    pod 'OAStackView'

    pod 'OHHTTPStubs/Swift'
end

target 'MyProject' do
    shared_pods
    pod 'OHHTTPStubs'
end

target 'MyProjectTests' do
    shared_pods
end

Test case

import XCTest
@testable import MyProject
import OHHTTPStubs

class ApiClientTests: XCTestCase {
    override func setUp() {
        super.setUp()
        stub(isPath("/api/notifications/")) { _ in
            let stubData = "Hello World!".dataUsingEncoding(NSUTF8StringEncoding)
            return OHHTTPStubsResponse(data: stubData!, statusCode:200, headers:nil)
        }
    }

    override func tearDown() {
        super.tearDown()
    }

    func testNotificationsWhenLoggedIn() {
        let api = ApiClient.sharedInstance()
        let expectation = expectationWithDescription("Alamofire")

        api.notifications() { response in
            XCTAssertTrue(response.success)
            expectation.fulfill()
        }

        waitForExpectationsWithTimeout(5.0, handler: nil)
    }

I'm using Alamofire and my notifications() method just makes a GET request to the stubbed path /api/notifications/

Any ideas?

@mikelupo
Copy link
Collaborator

We had to add -ObjC to the other linker flags in the app which makes the call. Then the stubs started working for us. We don't integrate via cocoa pods tho.

On Jul 22, 2016, at 8:16 PM, Igor Belo [email protected] wrote:

@AliSoftware even doing that I'm getting a hit in my API server...don't know if it's due an outdated version of CocoaPods (0.39.0) we are using


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

@AliSoftware
Copy link
Owner

AliSoftware commented Aug 17, 2016

Well looking at your code it seems that you properly integrated OHHTTPStubs to your app bundle and not your test bundle like it seems you wanted to do (which would make sense only if you use Hosted Tests, which you probably do as it's the default in Xcode now and otherwise you wouldn't be able to link to OHHTTPStubs in your test target anyway).

So now you'll have to ensure that Alamofire doesn't load itself and create its inner NSURLSession (which if does the first time you access Alamofire's shared Manager) before OHHTTPStubs had time to load into memory and install its stubbing protocol (breakpoints should help to check if that's the case or not)

@glyuck
Copy link

glyuck commented Sep 1, 2016

Ok, I've come across same issue yesterday, spent 4 hours debugging and... The problem was I've not included pod 'OHHTTPStubs' into my Podfile. I've had only pod 'OHHTTPStubs/Swift' there. Yes, it's stupid, yes it's documented in OHHTTPStubs' README. Hope it will help someone else. Once again, correct Podfile should look like this:

pod 'OHHTTPStubs'  # This one is required for Alamofire to work
pod 'OHHTTPStubs/Swift'

Probably it's an issue for @igorbelo because his podfile omits OHHTTPStubs pod for Tests target.

@johndpope
Copy link

maybe relevant to some - (the podspec doesn't actually work - doesn't look a pull request has been opened to cocoapods) https://github.com/michaelhayman/OHHTTPStubsExtensions

@ivanruizscm
Copy link

@glyuck same problem as you, i spend the whole day. I suggest adding auto dependency if pod 'OHHTTPStubs/Swift' just include pod 'OHHTTPStubs'.

@AliSoftware
Copy link
Owner

@ivanruizscm I already explained the rationale in other issues and PRs as to why OHHTTPStubs/Swift doesn't automatically include OHHTTPStubs (aka OHHTTPStubs/Default. That's because otherwise people wouldn't be able to opt-out of other subspecs that the default one automatically provides but which might not be relevant to everybody.

This is very well documented both in multiple places in the README.md, the wiki, and all the docs anyway (it's even at the beginning of the README right in the Installation paragraph, so it's hard to miss).

@AnandWalvekar
Copy link

Hi @AliSoftware
Thank you very much for the article. I have added OHHTTPStubs to App bundle and written stubs in didFinishLaunchingWithOptions method. But I want to move all the stubs added in didFinishLaunchingWithOptions to UI testing bundle's testMethods(). I tried accessing app OHHTTPStubs from the UI testing bundle as below but could not succeed.
let appOHHTTPStubs = Bundle.main.classNamed("OHHTTPStubs") appOHHTTPStubs?.stubRequests(...)
If we can't achieve what I am trying, is there a better way to move stubs code out of didFinishLaunchingWithOptions to improve the launch experiment while automating(we will off course not stub responses if app is not being launched from unit testing target)

@AliSoftware
Copy link
Owner

Hi @AnandWalvekar

As explained the way the test harness used by Xcode for hosted tests (and thus UI tests) works is that Xcode launches the app itself then run the UI test code from the context of the app. So if you want to apply stubs in your app requests while UI testing your app, then sadly given how Xcode execute those tests there's no real magical solution other than writing your stubbing code in your app target. The general trick to do that is to test in your code if an env var is set and only setup the stubs in that case. Then configure your Xcode scheme so that the test action defines that env var while the run action doesn't. Variants of that approach exists by basing the test wether to setup stubs or not on the command line arguments (instead of an env var), optionally leveraging the ArgumentDomain of UserDefaults (setting up your scheme to pass arguments like -uiTesting true only for the test action then reading the "uiTesting" user defaults to decide wether to stub or not)

@AnandWalvekar
Copy link

AnandWalvekar commented Apr 26, 2018

Did you say you need to remove "A workaround if you really need it" section from your article?

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

No branches or pull requests

10 participants