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

Stub not being called #47

Closed
brennon opened this issue Dec 29, 2013 · 31 comments
Closed

Stub not being called #47

brennon opened this issue Dec 29, 2013 · 31 comments

Comments

@brennon
Copy link

brennon commented Dec 29, 2013

I'm setting up a stub in an Specta spec that isn't getting called. Here's the code I'm using:

it(@"should request splashes", ^{
    __block BOOL success = NO ;

    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        return [request.URL.host isEqualToString:@"zamba.cs.vt.edu"];
    }
                        withStubResponse: ^OHHTTPStubsResponse *(NSURLRequest *request) {
                            NSData *stubData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"findSplashesResponse" ofType:@"txt"]];
                            NSDictionary *standardHeaders = @{ @"Content-Type":@"text/html"};
                            return [OHHTTPStubsResponse responseWithData:stubData statusCode:200 headers:standardHeaders];
                        }].name = @"findSplashes.php";
    [OHHTTPStubs setEnabled:YES];

    [client POST:@"findSplashes.php"
      parameters:@{ @"hash": [client hashString],
                    @"lat":[NSString stringWithFormat:@"%f", vt.coordinate.latitude],
                    @"long":[NSString stringWithFormat:@"%f", vt.coordinate.longitude],
                    @"radius":@"1600" }
         success:^(NSURLSessionDataTask *task, id responseObject) {
             NSString *response = [[NSString alloc] initWithData:(NSData *) responseObject encoding:NSUTF8StringEncoding];
             success = [response isEqualToString:@"252\t5527.5387976613\t4\t4\t1387655919\t1387568319\t300\t40.739983\t-73.992951\n"] ? YES : NO;
         }
         failure:^(NSURLSessionDataTask *task, NSError *error) {
             success = NO;
         }];

    expect(success).will.beTruthy();
});

client is a singleton instance of an AFHTTPSessionManager from AFNetworking 2.0. Strangely, this test and other similar tests were passing originally--I don't know what I've done to break them! When the POST request is made, I can check that OHHTTPStubs.sharedInstance is the same as when the stub was created. I've added the +[OHHTTPStubs setEnabled:] call per other issues here. The block passed as thestubRequestsPassingTest:` parameter is never executed--all requests hit the network. Any ideas? Thanks!

@bobbytables
Copy link

I'm experiencing the same issue with my project and have no idea why. I've NSLog'ed damn near everything and have not figured it out. So I'll contribute what I have since we're having the same issue.

iOS7
Afnetworking 2.0.3
OHHTTPStubs - 3.0.3

I've subclassed like this:

+ (instancetype)sharedClient {
    static ROAAPIClient *_sharedClient = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        NSURL *baseUrl = [NSURL URLWithString:API_BASEURL];
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

        _sharedClient = [[ROAAPIClient alloc] initWithBaseURL:baseUrl
                                         sessionConfiguration:config];
    });

    return _sharedClient;
}

I've setup my stub like this:

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        NSLog(@"------- %@ %@\n\n", request.URL.host, request.URL.path);
        return [request.URL.path isEqualToString:@"/oauth/token"];
    } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
        return [OHHTTPStubsResponse responseWithFileAtPath:OHPathForFileInBundle(@"successfulClientCredentials.json",nil)
                                                statusCode:200 headers:@{@"Content-Type":@"application/json"}];
    }];

I've logged OH hits like this:

[OHHTTPStubs onStubActivation:^(NSURLRequest *request, id<OHHTTPStubsDescriptor> stub) {
        NSLog(@"Hit stub for requested URL: %@", [request.URL absoluteString]);
    }];

I've also included an NSLog in the method of my client class that fires off the HTTP request. It does happen, and the success blocks fire correctly as well.

@bobbytables
Copy link

I created this demonstration project to maybe help mitigate this efficiently https://github.com/bobbytables/ohhttpstubs_47_bug

I'm logging out the response object, which when you run the tests, hits iTunes API (When it should be stubbed to return a json response file).

@bobbytables
Copy link

@brennon !!! Initialize your client after you setup the stub. Problem solved.

@AliSoftware
Copy link
Owner

@brennon I confirm @bobbytables answer : you initialize your client, using an NSURLSession and NSURLSessionConfiguration, before any call to OHHTTPStubs.

OHHTTPStubs swizzle the NSURLSessionConfiguration constructors so that they include the OHHTTPStubsProtocol that allows the stubbing to take place — so that when you create a [NSURLSessionConfiguration defaultSessionConfiguration] for example, it already support the OHHTTPStubsProtocol that make the stubbing possible if any — so it should work without any change from your part.

But it does this swizzling in its +initialize method (I can't do it in the +load method as when the OHHTTPStubs image is loaded there is no guaranty that the NSURLSessionConfiguration class and image is already loaded yet so I cannot interact with it safely).

So you have to use the OHHTTPStubs class at least once (sending it any arbitrary Objective-C message) — so that its +initialize method gets called — at the beginning of your code to be sure that any subsequent NSURLSessionConfiguration created will support stubbing.


In summary, initializing your client after you setup the stub solves the problem, but you can also still initialize you client before setting up your stub as long as you call any method on the OHHTTPStubs class before creating your client. This suppose it can be as simple as (void)[OHHTTPStubs class]; or [OHHTTPStubs setEnabled:YES]; as long as a method call is invoked, because the Objective-C Runtime won't call +initialize until the first message is sent to the class.

I'm closing this issue as the fix is given in my answer

@brennon
Copy link
Author

brennon commented Jan 3, 2014

Added a TEST #define to my main app target so that in my creation of the AFHTTPSessionManager I now have this:

+ (instancetype)sharedClientWithBaseURL:(NSURL *)url {
    static SplashAPIClient *sharedSplashAPIClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
#ifdef SPLASH_TEST
        [OHHTTPStubs setEnabled:YES];
#endif
        sharedSplashAPIClient = [[self alloc] initWithBaseURL:url];
    });
    return sharedSplashAPIClient;
}

I've confirmed that -[OHHTTPStubs setEnabled:] is being called with YES. However, now when the internals of AFNetworking now call +[NSURLSessionConfiguration defaultSessionConfiguration], this code in OHHTTPStubs+NSURLSessionConfiguration now acts up:

static NSURLSessionConfiguration* OHHTTPStubs_defaultSessionConfiguration(id self, SEL _cmd)
{
    NSURLSessionConfiguration* config = orig_defaultSessionConfiguration(self,_cmd); // call original method
    OHHTTPStubsAddProtocolClassToNSURLSessionConfiguration(config);
    return config;
}

In particular, the first line of the function body calls itself recursively, giving me a stack overflow... Am I missing something?

@brennon
Copy link
Author

brennon commented Jan 3, 2014

In fact, if the pod is compiled into the main target at all (irrespective of whether or not I actually make any calls to OHHTTPStubs), this happens.

@AliSoftware
Copy link
Owner

Given what you describe, it seems that the method swizzling is done twice, leading to the orig_defaultSessionConfiguration variable to point to the OHHTTPStubs_defaultSessionConfiguration method instead of the defaultSessionConfiguration original Apple method, hence the recursive call.

Can you place a breakpoint in the _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport method to confirm that it gets called twice (whereas it should only get called once), thus making the swizzling thice?
If it is indeed get called twice, can you report me the call stack in each case to understand which methods trigger those two calls and why it is then called twice instead of once?

I'll try to reproduce the bug on my side and add a protection against that in a future release but have to be sure about the reason first and be sure it will fix the issue for you too, so I'm interested in your feedback and debug informations.


And one more question: does this happen only if you have the OHHTTPStubs pod referenced in multiple targets in your Podfile, like in the main target AND the test target (but not if it is referenced only in one or the other) ? If so, this may be a bug in CocoaPods that creates two libraries for OHHTTPStubs (one for each target) and is probably related to this similar (closed) issue ?

@brennon
Copy link
Author

brennon commented Jan 4, 2014

Before I saw your message, I ripped out all the Pods and reinstalled them. Now, I can link against the library built from the Pod with no problem. However, now I am back to my original code. However, now this is the very first line of my app delegate's -application:didFinishLaunchingWithOptions::

[OHHTTPStubs setEnabled:YES];

Still, my stub (same as above) is not being called.

@brennon
Copy link
Author

brennon commented Jan 4, 2014

I wrote a pared down test to try to get a barebones failing scenario--now I'm back to the recursive swizzle...

From app delegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

#ifdef SPLASH_TEST
    [OHHTTPStubs setEnabled:YES];
#endif
    // ...
}

Test spec:

#import "Specta.h"
#define EXP_SHORTHAND
#import "Expecta.h"
#import "OCMock.h"
#import "SplashAPIClient.h"
#import "OHHTTPStubs.h"

SpecBegin(SplashAPIClient)

fdescribe(@"SplashAPIClient", ^{

    describe(@"basic functionality", ^{            
        fit(@"should pass a stupid test", ^{
            expect(YES).to.beTruthy();
        });

        fit(@"should f---ing work", ^{
            AFHTTPSessionManager *localManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://www.github.com"]];
        });
    });
});

SpecEnd

Stack trace for void _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport() first call:

Thread 1, Queue : com.apple.main-thread
#0  0x0007e96c in _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs+NSURLSessionConfiguration.m:69
#1  0x01c09275 in _class_initialize ()
#2  0x01c100f1 in lookUpImpOrForward ()
#3  0x01c1004e in _class_lookupMethodAndLoadCache3 ()

Stack trace for static NSURLSessionConfiguration* OHHTTPStubs_defaultSessionConfiguration(id self, SEL _cmd) first call:

Thread 1, Queue : com.apple.main-thread
#0  0x0007ea49 in OHHTTPStubs_defaultSessionConfiguration at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs+NSURLSessionConfiguration.m:55
#1  0x00075740 in -[AFURLSessionManager initWithSessionConfiguration:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m:294
#2  0x000608cb in -[AFHTTPSessionManager initWithBaseURL:sessionConfiguration:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m:70
#3  0x0006084a in -[AFHTTPSessionManager initWithBaseURL:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m:60
#4  0x000154e1 in -[SplashAPIClient initWithBaseURL:] at /Users/brennon/Development/splash-ios/splash/Source/Model/SplashAPIClient.m:33
#5  0x00015361 in __43+[SplashAPIClient sharedClientWithBaseURL:]_block_invoke at /Users/brennon/Development/splash-ios/splash/Source/Model/SplashAPIClient.m:27
#6  0x026f94b0 in _dispatch_client_callout ()
#7  0x026e8e17 in dispatch_once_f ()
#8  0x026e8d5c in dispatch_once ()
#9  0x00015242 in _dispatch_once [inlined] at /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/usr/include/dispatch/once.h:68
#10 0x000151e9 in +[SplashAPIClient sharedClientWithBaseURL:] at /Users/brennon/Development/splash-ios/splash/Source/Model/SplashAPIClient.m:26
#11 0x000150ef in +[SplashAPIClient sharedClient] at /Users/brennon/Development/splash-ios/splash/Source/Model/SplashAPIClient.m:20
#12 0x00034a22 in -[NearSplashesVC refreshSplashes] at /Users/brennon/Development/splash-ios/splash/Source/NearSplashesVC/NearSplashesVC.m:352
#13 0x00030a4f in -[NearSplashesVC mapView:regionDidChangeAnimated:] at /Users/brennon/Development/splash-ios/splash/Source/NearSplashesVC/NearSplashesVC.m:145
#14 0x00c39dc6 in -[MKMapView _didChangeRegionMidstream:] ()
#15 0x00c3bb11 in -[MKMapView _goToMapRegion:duration:animationType:resetHeading:] ()
#16 0x00c3c1e4 in -[MKMapView _setZoomScale:centerMapPoint:duration:animationType:resetHeading:] ()
#17 0x00c3b7c1 in -[MKMapView _setZoomScale:centerCoordinate:duration:animationType:resetHeading:] ()
#18 0x00c3e1e0 in -[MKMapView _goToCenterCoordinate:zoomLevel:animationType:cancelDefaultLocationTimer:] ()
#19 0x00c3d4f0 in -[MKMapView goToCenterCoordinate:zoomLevel:animationType:] ()
#20 0x00c41b3e in -[MKMapView goToRegion:animationType:] ()
#21 0x00c417a0 in -[MKMapView setRegion:animated:] ()
#22 0x0002f292 in -[NearSplashesVC viewDidLoad] at /Users/brennon/Development/splash-ios/splash/Source/NearSplashesVC/NearSplashesVC.m:56
#23 0x00eaf318 in -[UIViewController loadViewIfRequired] ()
#24 0x00eaf5b4 in -[UIViewController view] ()
#25 0x00ee63ae in -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] ()
#26 0x00ee5bd2 in -[UITabBarController transitionFromViewController:toViewController:] ()
#27 0x00ee1fbb in -[UITabBarController _setSelectedViewController:] ()
#28 0x00ee1de0 in -[UITabBarController setSelectedIndex:] ()
#29 0x00002955 in -[SplashController viewWillAppear:] at /Users/brennon/Development/splash-ios/splash/Source/SplashController.m:152
#30 0x00eb2bfa in -[UIViewController _setViewAppearState:isAnimating:] ()
#31 0x00eb3108 in -[UIViewController __viewWillAppear:] ()
#32 0x00eb40c7 in -[UIViewController viewWillMoveToWindow:] ()
#33 0x00df7384 in -[UIView(Hierarchy) _willMoveToWindow:withAncestorView:] ()
#34 0x00e02f60 in -[UIView(Internal) _addSubview:positioned:relativeTo:] ()
#35 0x00df69b1 in -[UIView(Hierarchy) addSubview:] ()
#36 0x00dd7bae in -[UIWindow addRootViewControllerViewIfPossible] ()
#37 0x00dd7d97 in -[UIWindow _setHidden:forced:] ()
#38 0x00dd802d in -[UIWindow _orderFrontWithoutMakingKey] ()
#39 0x0ffd1c66 in -[UIWindowAccessibility(SafeCategory) _orderFrontWithoutMakingKey] ()
#40 0x00de289a in -[UIWindow makeKeyAndVisible] ()
#41 0x00d95cd0 in -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] ()
#42 0x00d9a3a8 in -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] ()
#43 0x00dae87c in -[UIApplication handleEvent:withNewEvent:] ()
#44 0x00daede9 in -[UIApplication sendEvent:] ()
#45 0x00d9c025 in _UIApplicationHandleEvent ()
#46 0x038ad2f6 in _PurpleEventCallback ()
#47 0x038ace01 in PurpleEventCallback ()
#48 0x01e00d65 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
#49 0x01e00a9b in __CFRunLoopDoSource1 ()
#50 0x01e2b77c in __CFRunLoopRun ()
#51 0x01e2aac3 in CFRunLoopRunSpecific ()
#52 0x01e2a8db in CFRunLoopRunInMode ()
#53 0x00d99add in -[UIApplication _run] ()
#54 0x00d9bd3b in UIApplicationMain ()
#55 0x0003d2d5 in main at /Users/brennon/Development/splash-ios/splash/main.m:16

Stack trace for void _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport() second call:

Thread 1, Queue : com.apple.main-thread
#0  0x0007e96c in _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs+NSURLSessionConfiguration.m:69
#1  0x01c09275 in _class_initialize ()
#2  0x01c100f1 in lookUpImpOrForward ()
#3  0x01c1004e in _class_lookupMethodAndLoadCache3 ()

Stack trace for static NSURLSessionConfiguration* OHHTTPStubs_defaultSessionConfiguration(id self, SEL _cmd) second call:

Thread 1, Queue : com.apple.main-thread
#0  0x0007ea49 in OHHTTPStubs_defaultSessionConfiguration at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs+NSURLSessionConfiguration.m:55
#1  0x09d30ed0 in -[AFURLSessionManager initWithSessionConfiguration:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m:294
#2  0x09d1c05b in -[AFHTTPSessionManager initWithBaseURL:sessionConfiguration:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m:70
#3  0x09d1bfda in -[AFHTTPSessionManager initWithBaseURL:] at /Users/brennon/Development/splash-ios/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m:60
#4  0x09d17bb1 in __37-[SplashAPIClientSpec spt_defineSpec]_block_invoke16 at /Users/brennon/Development/splash-ios/splashTests/SplashAPIClientSpec.m:70
#5  0x09d549c9 in runExampleBlock at /Users/brennon/Development/splash-ios/Pods/Specta/src/SPTExampleGroup.m:70
#6  0x09d55b4b in __48-[SPTExampleGroup compileExamplesWithNameStack:]_block_invoke at /Users/brennon/Development/splash-ios/Pods/Specta/src/SPTExampleGroup.m:308
#7  0x09d58c1a in -[SPTXCTestCase spt_runExampleAtIndex:] at /Users/brennon/Development/splash-ios/Pods/Specta/src/SPTXCTestCase.m:95
#8  0x01e79d1d in __invoking___ ()
#9  0x01e79c2a in -[NSInvocation invoke] ()
#10 0x201032bf in -[XCTestCase invokeTest] ()
#11 0x2010338d in -[XCTestCase performTest:] ()
#12 0x09d592d7 in -[SPTXCTestCase performTest:] at /Users/brennon/Development/splash-ios/Pods/Specta/src/SPTXCTestCase.m:147
#13 0x2010417c in -[XCTest run] ()
#14 0x20102a44 in -[XCTestSuite performTest:] ()
#15 0x2010417c in -[XCTest run] ()
#16 0x20102a44 in -[XCTestSuite performTest:] ()
#17 0x2010417c in -[XCTest run] ()
#18 0x20102a44 in -[XCTestSuite performTest:] ()
#19 0x2010417c in -[XCTest run] ()
#20 0x20105aa1 in +[XCTestProbe runTests:] ()
#21 0x0066912c in __NSFireDelayedPerform ()
#22 0x01e43bd6 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()
#23 0x01e435bd in __CFRunLoopDoTimer ()
#24 0x01e2b628 in __CFRunLoopRun ()
#25 0x01e2aac3 in CFRunLoopRunSpecific ()
#26 0x01e2a8db in CFRunLoopRunInMode ()
#27 0x038ab9e2 in GSEventRunModal ()
#28 0x038ab809 in GSEventRun ()
#29 0x00d9bd3b in UIApplicationMain ()
#30 0x0003d2d5 in main at /Users/brennon/Development/splash-ios/splash/main.m:16

@brennon
Copy link
Author

brennon commented Jan 4, 2014

Good grief...

So, based on the above, it appears that OHHTTPStubs chokes when being asked to swizzle more than one (the one in my subclass of AFHTTPSession existed, as well.)

So, I stripped everything down again to this simple test (this is the only one being run in the entire suite:

    fit(@"should f---ing work", ^{
        __block BOOL stubWasCalled = NO;

        [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
            return YES;
        } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
            NSData *data = [@"workgoddammit" dataUsingEncoding:NSUTF8StringEncoding];
            return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:@{}];
        }].name = @"simple stub";

        DebugLog(@"All stubs: %@", [OHHTTPStubs allStubs]);

        [OHHTTPStubs onStubActivation:^(NSURLRequest *request, id<OHHTTPStubsDescriptor> stub) {
            stubWasCalled = YES;
        }];

        [[SplashAPIClient sharedClient] GET:@"findSplashes.php" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
            DebugLog(@"success");
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            DebugLog(@"failure");
        }];

        expect(stubWasCalled).to.beTruthy();
    });

DebugLog() is just a wrapped for NSLog(). If I break on that first DebugLog(), I can see that the stub is registered with [OHHTTPStubs allStubs]. I can also see this:

(lldb) po [SplashAPIClient sharedClient].session.configuration.protocolClasses
<__NSArrayI 0x9b9b2e0>(
OHHTTPStubsProtocol
)

So, the NSURLSessionConfiguration used for sharedClient only reports OHHTTPStubsProtocol. And no, the stub still isn't being called...

@AliSoftware
Copy link
Owner

Thanks for all those tests and reports, hopefully it will help me track this nasty issue.

I still can't understand how come the +initialize method can be called twice by the Runtime, whereas it is guarantied by Apple to be called only for the first method call of each class… this makes no sense to me, it's like there were two different OHHTTPStubs classes with the exact same name, ot that the Runtime's _class_lookupMethodAndLoadCache3 function was buggy and failed to see that it already initialized that class once before… very very odd…

I will try and reproduce the issue with the code you provide and investigate further ; unfortunately I am a bit busy lately and can't guaranty you that I'll have time to look into this this weekend.

@AliSoftware AliSoftware reopened this Jan 4, 2014
@samirGuerdah
Copy link

@AliSoftware : if a subclass does not implement +initialize but its superclass does, then that superclass’s +initialize will be invoked once per non-implementing subclass and once for itself.

You can find the detail. http://www.friday.com/bbum/2009/09/06/iniailize-can-be-executed-multiple-times-load-not-so-much/

@brennon
Copy link
Author

brennon commented Jan 4, 2014

Interestingly, if I place this test inside my app delegate's -application:didFinishLaunchingWithOptions:, and run the app target (not the test target), the stub is called:

__block BOOL stubWasCalled = NO;

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    return [request.URL.host isEqualToString:@"github.com"];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    NSData *data = [@"stub response string" dataUsingEncoding:NSUTF8StringEncoding];
    return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:@{@"Content-type":@"text/html"}];
}].name = @"simple stub";

DebugLog(@"All stubs: %@", [OHHTTPStubs allStubs]);

[OHHTTPStubs onStubActivation:^(NSURLRequest *request, id<OHHTTPStubsDescriptor> stub) {
    stubWasCalled = YES;
}];

AFHTTPSessionManager *localManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://github.com"]];

[localManager GET:@"/" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
    DebugLog(@"success");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    DebugLog(@"failure");
}];

@brennon
Copy link
Author

brennon commented Jan 4, 2014

For what it's worth, this is all I have to do in a test to descend into recursion hell

[OHHTTPStubs class];
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

Also, if I comment out all mention of OHHTTPStubs in my own code, as confirmed by a workspace-wide find, void _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport() is still called with the following stack trace:

Thread 1, Queue : com.apple.main-thread
#0  0x00097f62 in _OHHTTPStubs_InstallNSURLSessionConfigurationMagicSupport at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs+NSURLSessionConfiguration.m:69
#1  0x000989a2 in +[OHHTTPStubs initialize] at /Users/brennon/Development/splash-ios/Pods/OHHTTPStubs/OHHTTPStubs/Sources/OHHTTPStubs.m:111
#2  0x01c27275 in _class_initialize ()
#3  0x01c2e0f1 in lookUpImpOrForward ()
#4  0x01c2e04e in _class_lookupMethodAndLoadCache3 ()

Any clues?

@brennon
Copy link
Author

brennon commented Jan 4, 2014

I can confirm that the recursive loop only occurs when the pod is included for my main app target, as well. When it is only linked into the test target, I have no problems.

So, now I'm back to my original problem. I can confirm that the stub is called if I initialize my AFHTTPSessionManager subclass inside the test before stubbing a request. However, this requires that I initialize a separate client and cannot test the functionality of my singleton client, as the singleton is initialized on app launch. I can't fire up OHHTTPStubs inside my main app code (recursive loop...), nor should I have to do so. Either approach requires me to modify production code for only testing purposes--something I shouldn't have to do. How does one use OHHTTPStubs with a singleton client like this otherwise, though?

@AliSoftware
Copy link
Owner

@samir2303 I know that but unfortunately this cannot be the reason why the method is swizzled twice, as I already have this kind of protection against +initialize being called multiple times if a subclass does not implement it… Or maybe I have a problem in my protection code? But it does not seems like it to me…

That's too bad because that would indeed have been a good lead, so I would have preferred that I forgot about it so that we would have the explanation and solution to this issue… but as the protection is in place, there must be another reason why +initialize is called twice… but both time for the same [OHHTTPStubs class]! 😒

@AliSoftware
Copy link
Owner

@brennon could you maybe create a small project that highlight the issue and make it available so that we can easily test with the exact same conditions as yours? That would help us speed up the investigation and find what's wrong in OHHTTPStubs that causes this issue in your case.

Thx

@brennon
Copy link
Author

brennon commented Jan 4, 2014

Here you go: https://github.com/brennon/ohhttpstubs-issue47/. The commented-out test will not run as long as the OHHTTPStub pod is specified for the main target.

@AliSoftware
Copy link
Owner

I think I have a lead!

  • when I run the Unit Tests on one of my Xcode projet, the application:didFinishLoadingWithOptions: method of the application's AppDelegate is getting called (even as I am running the functionnal Unit Tests that have nothing to do with launching my real/complete application and showing its UI).
  • In there as I have a call to OHHTTPStubs, its +initialize method gets called, because that's the first time a message is sent to the OHHTTPStubs class by the hosting application
  • Then, my Unit Test code gets called, I there also have a call to OHHTTPStubs and its +initialize method is also called.

But both calls are from different bundles: one from the application and one from the Test bundle! (that's actually quite what was explain in the comment of the other issue but I didn't quite understood it all the way until then)

That's a special case of when you run Unit Tests and made them be hosted by your application:

  • The iPhone Simulator starts running your application (thus executing its application:didFinishLoadingWithOptions: code, which is code from your application and not your Test Suite / Test Bundle)
  • Then it loads the .xctest Test Bundle (quite like if it was a plugin to the app) in memory and starts executing its testXXX methods

I don't know if it can be considered as a bug of the iOS Simulator Runtime, as even if that's two different bundles/binary images loading the OHHTTPStubs in turn, that's a unique executable running the whole code so +initialize shouldn't get called a second time.

To avoid that behavior, and thus avoid the OHHTTPStubs class being loaded by both the host application (your app bundle's AppDelegate) and the test bundle, and +initialize being called twice, there is a simple solution: don't use your application as the target of your Unit Tests (which if I'm not mistaken is only useful if you want to do application tests, like GUI testing, but not functionnal unit testing, right?)

To do that:

  • Select you application's project in your workspace on the left, then select your Unit Tests target ("YouAppTests")
  • Select the "General" tab, then in the "Target" dropdown menu that appears, instead of selecting your App target, select "None"

Note that this is an issue that can happen with any other library/class that declare singletons and/or implement +initialize — it is not limited to OHHTTPStubs, and even if the solution I give above solves the issue by preventing Xcode to load both the application and the test bundls, having a protection in OHHTTPStubs's code would obviously be much better. Still trying to figure out a way to protect against the double-swizzling when that happen.

I will keep the issue open as I would like to investigate more on that later, maybe find a way to avoid the swizzling to be done twice if people use their app as the target or their UnitTests instead of "None", and add some warnings about all that in the README, but first we need to understand all that strange behavior better

@AliSoftware
Copy link
Owner

@brennon Thanks for the demo projet! But I don't understand how to use it: there is no Podfile in your repository (but there is Pods.xcconfig files, referenced in the xcodeproj but are missing), so I can't compile anything nor execute pod update… could you add them in your example repo so I can be sure to use the very same configuration as yours?

Also let me know if my solution above (changing the dropdown menu in the General tab of your UnitTest target to "None") solves your issue.

AliSoftware added a commit that referenced this issue Jan 5, 2014
…oth in the App target and the UnitTest targets, in the case UnitTests use the App as their host app (which is the default for new Xcode5 projects)

In such case, two OHHTTPStubs classes where loaded (one in the App Bundle, one in the Test bundle), leading to double swizzling, but only the first OHHTTPStubsProtocol (generally the one loaded by the app bundle) were used, leading to stubs from UnitTest bundle not being called.

To fix this, I now:

* The swizzling of `NSURLSessionConfiguration` is done using an `NSURLSessionConfiguration` category and its `+load` method to be done only once (much more logical by the way) to avoid double-swizzling (and a callstack overflow)
* The insertion of the `OHHTTPStubsProtocol` class in the `protocolClasses` property is done via `[OHHTTPStubs setEnabled:forSessionConfiguration:]` (instead of relying to `objc_getClass()`), to be sure that it uses the `OHHTTPStubsProtocol` class  loaded by the current `NSBundle` (and not the one returned by objc_getClass() which is generally the one from the `mainBundle` / App bundle)
@AliSoftware
Copy link
Owner

Hi again @brennon

I just did a commit in a attempt to try and solve this issue at last — even when your UnitTest target uses your app as a host (and loads the OHHTTPStubs twice, once in the app bundle and once in the test bundle), so even in the case you don't apply my suggestion in my comment above. Please tell me if it solves your issue 👍

This commit fixes two things:

  • The swizzling is now done in the +load method of an NSURLSessionConfiguration category, ensuring it only execute once instead of being executed as many times as the OHHTTPStubs class is loaded by the various bundles. This should solve the issue of the recursive call loop.
  • When the swizzled OHHTTPStubs_defaultSessionConfiguration function is called, I now use my [OHHTTPStubs setEnabled:forSessionConfiguration:] method directly instead of relying on objc_getClass(). This should solve the issue about the stubs not being called With the previous code, objc_getClass(@"OHHTTPStubsProtocol") would always returns the OHHTTPStubsProtocol that were loaded by the main bundle, which were only referencing stubs added from the main bundle and not stubs added in your Unit Tests that were referenced by the other OHHTTPStubsProtocol class — the one loaded by your xctest bundle… hence the issue

To try and use it, and confirm that it actually solves your problem, simply change your Podfile to point directly to my repo URL, so that CocoaPods loads the HEAD of my repo (instead of the latest public release):

pod "OHHTTPStubs", :git => "https://github.com/AliSoftware/OHHTTPStubs"

Then once you do a pod update CocoaPods should tell you that you now use OHHTTPStubs (3.0.4) (version of the HEAD of my repo) and you can test your code with it. I will only push this version 3.0.4 onto CocoaPods' public PodSpec repo — and make an official release — once you actually confirm that this really solves the issue and does not introduce any regression on your side.


This mess with the iOS simulator loading two different bundles and thus loading some classes more than once is a tricky one! I hope it solves your issue at last. Please keep me posted! 🍻

@brennon
Copy link
Author

brennon commented Jan 5, 2014

Sorry about that--I didn't commit add the pod-related files to the repository--have done so now...

I can confirm that your changes in your current commit do fix the issues in the test repo that I referenced. I don't know if (but I assume that) they do fix the issue in my 'real' project.

This, however, doesn't really address the other issue that I mentioned... Where someone is in the situation that they use a singleton class for NSURLSession-related functionality, and they're doing GUI-related testing in their tests (which I'm doing), is there any way to do so without modifying production code? As it stands now, I have to create a different scheme that allows me to create a different preprocessor #define, that allows me to ensure that OHHTTPStubs is initialized before any NSURLSession-related calls in my tests. I'd really prefer to leave all of my production code pristine and keep all code required for testing in my test target, but as of now I really don't see any way to do that...

@AliSoftware
Copy link
Owner

@brennon are you sure about that?

The change I made, namely doing the swizzling in the +load method of a category of NSURLSessionConfiguration instead of in +initialize of OHHTTPStubs now allows the swizzling to take place very early and even if you never call any method of OHHTTPStubs before creating your first NSURLSessionConfigurations.

That way, you can now call [NSURLSessionConfiguration defaultConfiguration] even in the early stages of your code and in your production code without the need to call any OHHTTPStubs method beforehand and without changing anything in your production code now.


The only thing to keep in mind is that when you do GUI-testing — and thus select your app target in the "Target" dropdown menu in the Test Target — and have the OHHTTPStubs pod in both your targets, then you end up with two different OHHTTPStubs (and OHHTTPStubsProtocol) loaded in memory (one in each bundle), so you have kind of two independent sets of stubs (and your NSURLSessionConfigurations will register the two different OHHTTPStubsProtocol classes — one from each bundle —thus testing the stubs installed by the application bundle first, then the stub created by the test bundle.

That's not really related to OHHTTPStubs itself directly, but rather to the way Xcode loads the bundles to perform GUI testing, loading the app bundle first then the test bundle. Thus loading the external libraries in both bundles (if they are referenced in each bundle) creating two duplicate but independent classes (thus two different singletons etc.)

So as a result, all stubs registered in the OHHTTPStubs loaded by the app bundle will be tested first, then only after that will stubs registered in the OHHTTPStubs loaded by the test bundle will be tested. So stubs installed by the test bundle won't override/preempt stubs installed by the app bundle.
But that's quite logical if you think about it, as stubs created in the application bundle are part of the application itself (like to stub a server API that is not available yet)… so it makes sense that if you have stubs created by your application code, those are tested too by your test code, right?


If you really want to manipulate (disable/remove/…) stubs installed by your application code, so that they don't get in the way of the stubs you create in your test suites, you may use the Class returned by [[NSBundle mainBundle] classNamed:@"OHHTTPStubs"] (instead of using the OHHTTPStubs token directly) to manipulate them from your test suite:

- (void)setUp
{
    // Get the OHHTTPStubs that is loaded in the application's bundle
    Class AppOHHTTPStubs = [[NSBundle mainBundle] classNamed:@"OHHTTPStubs"];
    // and remove all its stub as we want to use only the stub we will setup in our Test Suite
    [AppOHHTTPStubs removeAllStubs];
}

  • TODO: Write a wiki page about that tricky case and talk about it in the README

@AliSoftware
Copy link
Owner

I just did the following test that confirms what I just explained:

  • I create an AFURLSessionManager instance in application:didFinishLaunchingWithOptions:, and store it in an AppDelegate's @property(strong) sessionManager.
  • I have an UIButton in my interface whose associated IBAction calls [self.sessionManager GET:parameters:success:failure:
  • I also have a Test Suite that calls [OHHTTPStubs stubRequestsPassingTest:withStubResponse:] to create a stub, then calls the very same IBAction directly (to fake a tap on the button, that's the GUI-related test).

As a result:

  • When I run my application normally and tap on the UIButton, the real request gets called. (If I had created an stub in application:didFinishLaunchingWithOptions:, that stub would have been called instead)
  • But when I run my Test Suite, the stub created in my Test Suite gets called. In details:
    • The NSURLSessionConfiguration(OHHTTPStubs)'s +load method gets called automatically and do the swizzling
    • Then the application:didFinishLaunchingWithOptions: method gets called and create the AFURLSessionManager instance. Thanks to the swizzling the NSURLSessionConfiguration created by this AFURLSessionManager supports the OHHTTPStubsProtocol transparently — even if you have never called any method on OHHTTPStubs yet.
    • Then the Test Suite is executed, creating the stub then calling the IBAction — that uses the AFURLSessionManager to send a network request — and the request is stubbed there.

And all that without having to change a thing in the original application's code that you can keep untouched. So that seems to fit the use case you need, right?

@brennon
Copy link
Author

brennon commented Jan 9, 2014

Seems to work now--thanks!

@AliSoftware
Copy link
Owner

Cool !

I just released version 3.0.4 (see here).

I will now have to write a dedicated article to explain this tricky case in details, in case other users have the same problem of having to disable the AppDelegate's stubs when running their Application Tests…

@JustinDSN
Copy link

@brennon @AliSoftware

Can you update the article on how to integrate OHHTTPStubs into your Test target and also use it to stub requests from the app target. I'd like to add UISwitches to development builds of my app to turn on the stubbing behaviors when the app is running in the simulator and on a device. I couldn't figure out how to do it from this issue.

I would like to do exactly what you're doing in the demo project. But the wiki pages said to NOT link the OHHTTPStubs project to your application target. So there's a gap in the instructions on how to accomplish what the demo project does using the pod file.

Thank you,
Justin

@AliSoftware
Copy link
Owner

@JustinDSN thanks for the reminder, I totally forgot about that article that I had to update.

I just wrote every details in it and I hope it covers every use case.

In summary:

  • Either you really need Hosted Tests that launches the app before loading the test bundle (because you are doing UI testing) and in that case don't link any library (OHHTTPStubs included) in both targets because they will end up being loaded twice. Instead, only link those libs with the app. But that's probably not your case
  • Or you don't do UI testing and then you don't need Hosted Tests. In that case select "None" from the "General" tab of your Test target, and then your app bundle won't be loaded when you run your tests and you may link OHHTTPStubs with both the app and test targets, as those targets will be truly independent then and the app bundle won't be loaded when the tests are run.

AliSoftware added a commit that referenced this issue Nov 3, 2015
…oth in the App target and the UnitTest targets, in the case UnitTests use the App as their host app (which is the default for new Xcode5 projects)

In such case, two OHHTTPStubs classes where loaded (one in the App Bundle, one in the Test bundle), leading to double swizzling, but only the first OHHTTPStubsProtocol (generally the one loaded by the app bundle) were used, leading to stubs from UnitTest bundle not being called.

To fix this, I now:

* The swizzling of `NSURLSessionConfiguration` is done using an `NSURLSessionConfiguration` category and its `+load` method to be done only once (much more logical by the way) to avoid double-swizzling (and a callstack overflow)
* The insertion of the `OHHTTPStubsProtocol` class in the `protocolClasses` property is done via `[OHHTTPStubs setEnabled:forSessionConfiguration:]` (instead of relying to `objc_getClass()`), to be sure that it uses the `OHHTTPStubsProtocol` class  loaded by the current `NSBundle` (and not the one returned by objc_getClass() which is generally the one from the `mainBundle` / App bundle)
@nicolasmiari-unext
Copy link

nicolasmiari-unext commented May 23, 2017

I already have the host application set to "None" (I'm testing a Framework).
The test block is called for requests other than my own (e.g. host: static.realm.io), but not for mine. Instead, I get NSURLErrorCancelled...

I am using a background session, but I am also calling

OHHTTPStubs.setEnabled(true, for: myBackgroundSessionConfiguration)

before setting up any stubs and before initializing the session.


Update: I tried changing my session configuration to default and now the blocks (passingTest and stubResponse) are executed.

Either the stubs don't work with background sessions, or my code invalidating the background session on each test isn't working as expected...

@AliSoftware
Copy link
Owner

AliSoftware commented May 23, 2017

@nicolasmiari-unext Background sessions are handled by the system itself, we can't hook on them as they are performed out-of-process and thus out of reach for interception. See the "Known Limitations" section in the README for more info.

@nicolasmiari-unext
Copy link

nicolasmiari-unext commented May 23, 2017

@AliSoftware Thank you, I missed that.
I'll be using the default session configuration for my unit tests (Can't unit-test background downloads anyway).

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

6 participants