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

Nocilla support for NSURLSessionDataTask #50

Closed
johanforssell opened this issue Sep 11, 2013 · 9 comments
Closed

Nocilla support for NSURLSessionDataTask #50

johanforssell opened this issue Sep 11, 2013 · 9 comments

Comments

@johanforssell
Copy link

I'm trying out Nocilla with AFNetworking 2.0 RC2 and it's not working. Am I missing something or does Nocilla not support NSURLSession and all the other new goodies in iOS7?

Here's an example, which does not work as I assumed it would

#import <XCTest/XCTest.h>
#import <Nocilla/Nocilla.h>
#import <AFNetworking/AFHTTPClient.h>

#define TestNeedsToWaitForBlock() __block BOOL blockFinished = NO
#define BlockFinished() blockFinished = YES
#define WaitForBlock() while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) && !blockFinished)

@interface TestNocilla : XCTestCase
@end



@implementation TestNocilla

- (void)setUp
{
    [super setUp];
    [[LSNocilla sharedInstance] start];

}

- (void)tearDown
{
    [[LSNocilla sharedInstance] stop];
    [super tearDown];
}


- (void)testB
{
    stubRequest(@"GET", @"http://www.google.com/nonsense/path");

    TestNeedsToWaitForBlock();

    AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:@"http://www.google.com"]];
    NSMutableURLRequest *request = [client requestWithMethod:@"GET" URLString:@"/nonsense/path" parameters:nil];

    NSURLSessionDataTask *task = [client dataTaskWithRequest:request success:^(NSURLResponse *response, id responseObject) {
        NSLog(@"success: %@", response);
        BlockFinished();
    } failure:^(NSError *error) {
        NSLog(@"fail: %@", error);
        BlockFinished();
        XCTFail(@"If Nocilla managed to stub the http call, this should not have happened");
    }];

    [task resume];
    WaitForBlock();

}

@end
@raphaeloliveira
Copy link

+1

@spekke
Copy link

spekke commented Sep 27, 2013

I'm having the same problem, but I think the problem is not with Nocilla. It seems like NSURLSession and NSURLProtocol doesn't play well together under a testing environment.

In the example below the method + (BOOL)canInitWithRequest: is never called.

#import <XCTest/XCTest.h>

#define TestNeedsToWaitForBlock() __block BOOL blockFinished = NO
#define BlockFinished() blockFinished = YES
#define WaitForBlock() while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) && !blockFinished)

@interface NSURLProtocolImpl : NSURLProtocol
@end

@implementation NSURLProtocolImpl

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    NSLog(@"NSURLProtocolImpl canInitWithRequest: %@", request);
    return NO;
}

@end


@interface NSUrlProtocolTests : XCTestCase

@end

@implementation NSUrlProtocolTests

- (void)setUp
{
    [super setUp];
    [NSURLProtocol registerClass:[NSURLProtocolImpl class]];
}

- (void)tearDown
{
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testExample
{

    TestNeedsToWaitForBlock();

    NSURL *url = [NSURL URLWithString:@"http://www.example.com"];

    NSURLSession *urlsession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDataTask *datatask = [urlsession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if(error) {
            NSLog(@"error: %@", error);
        }
        else {
            NSLog(@"Success");
        }
        BlockFinished();
    }];
    [datatask resume];

    WaitForBlock();
}

@end

This was tested with XCode 5.0 (5A1412) and iOS 7 simulator.

@gdunkle
Copy link

gdunkle commented Oct 4, 2013

I believe the reason this is not working out of the box is because you have to register the LSHTTPStubURLProtocol via the NSUSerSessionConfiguration's protocolClasses array instead of using the registerClass method (see https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/Reference/Reference.html#//apple_ref/doc/uid/TP40013440-CH1-SW24).

So something like this in your test setup

...
NSURLSessionConfiguration    configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

configuration.protocolClasses=@[[LSHTTPStubURLProtocol class]];
...

@michaelmcguire
Copy link

@gdunkle is correct that the LSHTTPStubURLProtocol is not properly getting registered when using NSURLSession. However, there does not appear to be a way to globally register it for all instances of NSURLSession. This is one of the advantages of NSURLSession is that each instance can have its own set of configuration settings, cookies stores, etc.

This means getting this to work in testing is more difficult. You cannot modify the defaults in the way above because defaultsessionConfiguration returns a copy so modifying the copy does not change the configuration for all users of the class. Instead, you will need to allow the configuration to be set or injected into the class you are testing.

Unfortunately, if you are using Nocilla for automated UI testing, you'll need to figure out how you want to do that for only when running tests (I'm using a preprocessing statements, which I'm not happy with).

@gdunkle
Copy link

gdunkle commented Oct 30, 2013

Yeah it's non trivial but definitely doable (at least with AFNetworking). I have it working for my tests by first extending AFHTTPSessionManager

@interface HttpSessionManager : AFHTTPSessionManager
@end

@implementation HttpSessionManager
    -(instancetype)init{
         //this retrieval of base url is specific to my implementation the main thing is you call the super constructor that you're wrapping in the category below
         NSString *baseUrl = [[Properties instance]config][@"baseUrl"];
         self = [super initWithBaseURL:[NSURL URLWithString:baseUrl]];
         if(!self){
            return nil;
         }
         return self;
    }
@end

Then in my test I use a category to add a wrapper around the init method of HttpSessionManager and swizzle the initWithBaseURL and wrap_initWithBaseURL

@interface  HttpSessionManager (AddTestProtocols)
@end

@implementation  HttpSessionManager (AddTestProtocols)
- (instancetype)wrap_initWithBaseURL:(NSURL *)url  sessionConfiguration:(NSURLSessionConfiguration *)configuration{
    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }
    //set the protocol class here!
    configuration.protocolClasses=@[[LSHTTPStubURLProtocol class]];

    return [self wrap_initWithBaseURL:url sessionConfiguration:configuration];
}

//swizzle the initWithBaseURL and wrap_initWithBaseURL 
+ (void)load
{
    Method original, swizzled;

    SEL originalSelector=@selector(initWithBaseURL:sessionConfiguration:);
    SEL swizzledSelector=@selector(wrap_initWithBaseURL:sessionConfiguration:);
    original = class_getInstanceMethod(self, originalSelector);
    swizzled = class_getInstanceMethod(self, swizzledSelector);
    method_exchangeImplementations(original, swizzled);
}
@end

Of course my example is using AFNetworking's extension of the NSUrlSession stuff but I imagine the same principals could be applied.

This method has been working quite well for me.

timshadel added a commit to timshadel/Nocilla that referenced this issue Nov 8, 2013
Helps allow users to work with `NSURLSession` instances, like luisobo#50.
@timshadel
Copy link
Contributor

My solution ended up being pretty simple. I already have a class which manages my AFHTTPSessionManager, so I simply added a method to hijack it.

Network.m

+ (AFHTTPSessionManager *)api
{
    return [Network sharedInstance].apiSession;
}

// Used with testing
- (void)hijackSessionWithProtocolClasses:(NSArray *)protocolClasses
{
    NSURLSessionConfiguration *hijackedConfig = self.apiSession.session.configuration;
    hijackedConfig.protocolClasses = protocolClasses;
    [self.apiSession invalidateSessionCancelingTasks:YES];
    self.apiSession = [[AFHTTPSessionManager alloc] initWithBaseURL:self.apiURL sessionConfiguration:hijackedConfig];
}

Then in my tests I just make sure to hijack it:

MyAPIControllerTests.m

- (void)setUp
{
    [[Network sharedInstance] hijackSessionWithProtocolClasses:@[[LSHTTPStubURLProtocol class]]];
}

And then everything worked just fine. I had some trouble referencing LSHTTPStubURLProtocol, since the podspec didn't expose it. But once I changed the spec locally everything was great. I've submitted #54 to update the podspec to let others do something similar.

@jdx
Copy link

jdx commented Jan 14, 2014

@timshadel's fix worked for me! Would be awesome to get this included so we could use AFHTTPSessionManager out of the box!

@kreeger
Copy link

kreeger commented Jan 29, 2014

@timshadel looks like your fix was turned down in order to have issue #60 implemented. I can't seem to figure out how to get the solution in #60 to catch my NSURLSession-driven requests. Shouldn't Nocilla raise an NocillaUnexpectedRequest exception if any request calls out that's not stubbed? Because it doesn't seem to be working for me.

MyClass below is a subclass of AFNetworking's AFHTTPSessionManager and it makes a POST call inside the implementation. No big surprises there.

describe(@"MyClass", ^{
    beforeAll(^{
        [[LSNocilla sharedInstance] start];
    });
    afterEach(^{
        [[LSNocilla sharedInstance] clearStubs];
    });
    afterAll(^{
        [[LSNocilla sharedInstance] stop];
    });
    it(@"does the thing", ^AsyncBlock{
        [MyClass makeThatAPICallWithCompletion:^(NSURLSessionDataTask *task, id data, NSError *error) {
            NSInteger statusCode = [(NSHTTPURLResponse *)task.response statusCode];
            expect(data).willNot.beNil();
            expect(statusCode).will.equal(200);
            done();
        }];
    });
});

It lets the request go through just fine; no exception is raised. Do I need to register LSNSURLSessionHook as a hook with Nocilla, similar to how LSASIHTTPRequestHook is registered in the readme?

[[LSNocilla sharedInstance] registerHook:[[LSASIHTTPRequestHook alloc] init]];

LSNSURLSessionHook.h file isn't even exposed through the pod, it seems.

Edit: stranger still, when Travis CI Pro checks out and builds my project, it raises the exception… but not on my local machine, not when running the exact same command that Travis runs (which is an xctool command).

@luisobo
Copy link
Owner

luisobo commented Jan 29, 2014

@kreeger can you open a new issue with a sample project, please?

I'm closing this issue since it should work as of #60, release in 0.8.0. Please open new issues with any questions or well... issues.

Thanks.

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

9 participants