Skip to content

Testing for the request body in your stubs

mplorentz edited this page Oct 2, 2017 · 26 revisions

The issue with HTTPBody

As seen in issue #52, unfortunately when you send POST requests (with a body) using NSURLSession, by the time the request arrives to OHHTTPStubs, the HTTPBody of the NSURLRequest can't be accessed anymore (probably already transformed internally to an HTTPBodyStream?). This is a known Apple bug.

Therefore, you cannot directly test the request.HTTPBody in your [OHHTTPStubs stubRequestsPassingTest:withStubResponse:] as the HTTPBody property will return nil at that time.

The workaround

Since version 5.1.0 and thanks to the PR #166 submitted by @felixLam and @shagedorn, you can now check the property OHHTTPStubs_HTTPBody (ohhttpStubs_httpBody in Swift) instead when you need to test for the HTTPBody.

This property is provided as an extension/category on the NSURLRequest class which contains a copy of the HTTPBody but which, contrary to the HTTPBody property, isn't reset to nil by iOS by the time you test the NSURLRequest in your stubs. This way you can now check the content of that NSData and return different stubs depending on them too.

⚠️ If you want to be able to access this property, don't forget to #import <OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h> though 😉

Example

#import <OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h>
…
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    // Only stub POST requests with a body equal to "user=foo&password=bar"
    NSData* body = request.OHHTTPStubs_HTTPBody;
    NSString* bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];

    return [request.HTTPMethod isEqualToString:@"POST"]
        && [bodyString isEqualToString:@"user=foo&password=bar"];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    return [OHHTTPStubsResponse responseWithData:validLoginResponseData statusCode:200 headers:headers];
}].name = @"login";

Note: One known limitation to this workaround is that if you set the HTTPBody and later reset it to nil (for whatever reason), OHHTTPStubs_HTTPBody will still return the last non-nil value. That is a case that is generally very rare, though.


Storing requests meta-data to write easier tests

Another nice trick to know is the existence of [NSURLProtocol setProperty:forKey:inRequest:], which allows you to associate arbitrary key/value pairs to your NSURLRequests, that you can later query using [NSURLProtocol propertyForKey:inRequest:].

Thus you can easily use this method to store any meta data you want to associate with your request, like the parameters dictionary used to build the query part of your GET URLs, or the RPC method that your request intends to call, etc. This will then allow you to use those meta-data to conditionally stub your requests with easier conditional checks (like retrieve the RPC method you previously associated with that NSURLRequest in your OHHTTPStubs test block and check on it to decide which stub to return, for example).

Example using AFNetworking 2.0+

If you are using AFNetworking 2.0+ to build your request, you can provide a custom AFHTTPRequestSerializer class to associate some properties to your requests when building them.

Here is an example (in ObjC, but you could easily translate it to Swift) on how to do that to automatically associate the parameters and body to each request build using the RequestSerializer.

1. Create a new AFHTTPRequestSerializer subclass

// .h
extern NSString* const NSURLRequestParametersMetadataKey;
@interface AnnotatedRequestSerializer : AFHTTPRequestSerializer @end

// .m
NSString* const NSURLRequestParametersMetadataKey = @"parameters";
@implementation AnnotatedRequestSerializer
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(NSDictionary *)parameters
                                     error:(NSError * __autoreleasing *)error
{
    NSMutableURLRequest* req = [super requestWithMethod:method URLString:URLString parameters:parameters error:error];
    [NSURLProtocol setProperty:parameters forKey:NSURLRequestParametersMetadataKey inRequest:req];
    // And why not any other meta-data you would want to compute and auto-add
    return req;
}
@end

2. Use it with your AFHTTPSessionManager

sessionMgr = [[AFHTTPSessionManager alloc] initWithBaseURL:sessionConfiguration:…];
sessionMgr.requestSerializer = [AnnotatedRequestSerializer serializer];

3. Send your request as usual

[sessionMgr POST:@"/login"
      parameters:@{ @"login":@"foo", @"password":@"bar" }
         success:failure:…
];

4. Write your stubs

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    NSDictionary* requestParams = [NSURLProtocol propertyForKey:NSURLRequestParametersMetadataKey inRequest:request];
    // Here you can also retrieve any other meta-data that you'd have added in your implementation in step 1
    // (in case you decided to write some code to add other custom meta-data automatically to all AFN requests)

    // Only stub POST requests to "/login" with "user" = "foo" & "password" = "bar"
    return [request.HTTPMethod isEqualToString:@"POST"]
        && [request.URL.path isEqualToString:@"/login"]
        && [requestParams[@"user"] isEqualToString:@"foo"]
        && [requestParams[@"password"] isEqualToString:@"bar"];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    return [OHHTTPStubsResponse responseWithData:validLoginResponseData statusCode:200 headers:headers];
}].name = @"login";