i am making a GET request to retrieve JSON data with AFNetworking as this code below :
NSURL *url = [NSURL URLWithString:K_THINKERBELL_SERVER_URL];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
Account *ac = [[Account alloc]init];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[NSString stringWithFormat:#"/user/%#/event/%#",ac.uid,eventID] parameters:nil];
AFHTTPRequestOperation *operation = [httpClient HTTPRequestOperationWithRequest:request
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSError *error = nil;
NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingAllowFragments error:&error];
if (error) {
}
[self.delegate NextMeetingFound:[[Meeting alloc]init] meetingData:JSON];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error){
}];
[httpClient enqueueHTTPRequestOperation:operation];
the thing is i want to create a unit test based on this data, but i dont want that the test will actually make the request. i want a predefined structure will return as the response. i am kind'a new to unit testing, and poked a little of OCMock but cant figure out how to manage this.
Several things to comment about your question.
First of all, your code is hard to test because it is creating the AFHTTPClient directly. I don't know if it's because it's just a sample, but you should inject it instead (see the sample below).
Second, you are creating the request, then the AFHTTPRequestOperation and then you enqueue it. This is fine but you can get the same using the AFHTTPClient method getPath:parameters:success:failure:.
I do not have experience with that suggested HTTP stubbing tool (Nocilla) but I see it is based on NSURLProtocol. I know some people use this approach but I prefer to create my own stubbed response objects and mock the http client like you see in the following code.
Retriever is the class we want to test where we inject the AFHTTPClient.
Note that I am passing directly the user and event id, since I want to keep things simple and easy to test. Then in other place you would pass the accout uid value to this method and so on...
The header file would look similar to this:
#import <Foundation/Foundation.h>
#class AFHTTPClient;
#protocol RetrieverDelegate;
#interface Retriever : NSObject
- (id)initWithHTTPClient:(AFHTTPClient *)httpClient;
#property (readonly, strong, nonatomic) AFHTTPClient *httpClient;
#property (weak, nonatomic) id<RetrieverDelegate> delegate;
- (void) retrieveEventWithUserId:(NSString *)userId eventId:(NSString *)eventId;
#end
#protocol RetrieverDelegate <NSObject>
- (void) retriever:(Retriever *)retriever didFindEvenData:(NSDictionary *)eventData;
#end
Implementation file:
#import "Retriever.h"
#import <AFNetworking/AFNetworking.h>
#implementation Retriever
- (id)initWithHTTPClient:(AFHTTPClient *)httpClient
{
NSParameterAssert(httpClient != nil);
self = [super init];
if (self)
{
_httpClient = httpClient;
}
return self;
}
- (void)retrieveEventWithUserId:(NSString *)userId eventId:(NSString *)eventId
{
NSString *path = [NSString stringWithFormat:#"/user/%#/event/%#", userId, eventId];
[_httpClient getPath:path
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSDictionary *eventData = [NSJSONSerialization JSONObjectWithData:responseObject options:0 error:NULL];
if (eventData != nil)
{
[self.delegate retriever:self didFindEventData:eventData];
}
}
failure:nil];
}
#end
And the test:
#import <XCTest/XCTest.h>
#import "Retriever.h"
// Collaborators
#import <AFNetworking/AFNetworking.h>
// Test support
#import <OCMock/OCMock.h>
#interface RetrieverTests : XCTestCase
#end
#implementation RetrieverTests
- (void)setUp
{
[super setUp];
// Put setup code here; it will be run once, before the first test case.
}
- (void)tearDown
{
// Put teardown code here; it will be run once, after the last test case.
[super tearDown];
}
- (void) test__retrieveEventWithUserIdEventId__when_the_request_and_the_JSON_parsing_succeed__it_calls_didFindEventData
{
// Creating the mocks and the retriever can be placed in the setUp method.
id mockHTTPClient = [OCMockObject mockForClass:[AFHTTPClient class]];
Retriever *retriever = [[Retriever alloc] initWithHTTPClient:mockHTTPClient];
id mockDelegate = [OCMockObject mockForProtocol:#protocol(RetrieverDelegate)];
retriever.delegate = mockDelegate;
[[mockHTTPClient expect] getPath:#"/user/testUserId/event/testEventId"
parameters:nil
success:[OCMArg checkWithBlock:^BOOL(void (^successBlock)(AFHTTPRequestOperation *, id))
{
// Here we capture the success block and execute it with a stubbed response.
NSString *jsonString = #"{\"some valid JSON\": \"some value\"}";
NSData *responseObject = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
[[mockDelegate expect] retriever:retriever didFindEventData:#{#"some valid JSON": #"some value"}];
successBlock(nil, responseObject);
[mockDelegate verify];
return YES;
}]
failure:OCMOCK_ANY];
// Method to test
[retriever retrieveEventWithUserId:#"testUserId" eventId:#"testEventId"];
[mockHTTPClient verify];
}
#end
The last thing to comment is that the AFNetworking 2.0 version is released so consider using it if it covers your requirements.
Related
I've subclassed AFHTTPSessionManager according to the recommended best practice for iOS 8 (in place of AFHTTPOperationManager, which I was using before).
I can grab the NSHTTPURLResponse from the task (except that has no body, only headers), and the callback returns the serialized responseObject which is fine.
Sometimes I need to log the response as a string or display it in a text field - there doesn't appear to be a way to do this natively using SessionManager? OperationManager allowed you to reference the raw response as an NSString:
operation.responseString;
I suppose I could stringify the serialized requestObject, but that seems like a lot of unnecessary overhead, and won't help if the response object is invalid JSON.
Here's my subclassed singleton:
#implementation MyAFHTTPSessionManager
+ (instancetype)sharedManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
And then to make a simple GET (which I've added to a block method), I can do:
[[MyAFHTTPSessionManager sharedManager] GET:_url parameters:queryParams success:^(NSURLSessionDataTask *task, id responseObject) {
completion(YES, task, responseObject, nil);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
completion(NO, task, nil, error);
}];
You can accomplish this by creating a custom response serializer that records the data and serializes the response using the standard response serializer, combining both the raw data and parsed object into a custom, compound response object.
#interface ResponseWithRawData : NSObject
#property (nonatomic, retain) NSData *data;
#property (nonatomic, retain) id object;
#end
#interface ResponseSerializerWithRawData : NSObject <AFURLResponseSerialization>
- (instancetype)initWithForwardingSerializer:(id<AFURLResponseSerialization>)forwardingSerializer;
#end
...
#implementation ResponseWithRawData
#end
#interface ResponseSerializerWithRawData ()
#property (nonatomic, retain) forwardingSerializer;
#end
#implementation ResponseSerializerWithRawData
- (instancetype)initWithForwardingSerializer:(id<AFURLResponseSerialization>)forwardingSerializer {
self = [super init];
if (self) {
self.forwardingSerializer = forwardingSerializer;
}
return self;
}
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error {
id object = [self.forwardingSerializer responseObjectForResponse:response data:data error:error];
// TODO: could just log the data here and then return object; so that none of the request handlers have to change
if (*error) {
// TODO: Create a new NSError object and add the data to the "userInfo"
// TODO: OR ignore the error and return the response object with the raw data only
return nil;
} else {
ResponseWithRawData *response = [[ResponseWithRawData alloc] init];
response.data = data;
response.object = object;
return response;
}
}
#end
Then set this serializer on your session manager:
#implementation MyAFHTTPSessionManager
+ (instancetype)sharedManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
instance.responseSerializer = [[ResponseSerializerWithRawData alloc] initWithForwardingSerializer:instance.responseSerializer];
});
return instance;
}
Now in your completion handler you will get an instance of ResponseWithRawData:
[[MyAFHTTPSessionManager sharedManager] GET:_url parameters:queryParams success:^(NSURLSessionDataTask *task, id responseObject) {
ResponseWithRawData *responseWithRawData = responseObject;
NSLog(#"raw data: %#", responseWithRawData.data);
// If UTF8 NSLog(#"raw data: %#", [[NSString alloc] initWithData:responseWithRawData.data encoding:NSUTF8StringEncoding]);
// TODO: do something with parsed object
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
I just whipped this up without compiling/testing, so I will leave it to you to debug and fill in the gaps.
You can access the “data” object directly from AFNetworking by using the “AFNetworkingOperationFailingURLResponseDataErrorKey” key so there is no need for subclassing the AFJSONResponseSerializer. You can the serialize the data into a readable dictionary. Here is some sample code to get JSON Data :
NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
NSDictionary *serializedData = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil];
Here is code to Get Status code in Failur block
NSHTTPURLResponse* r = (NSHTTPURLResponse*)task.response;
NSLog( #"success: %d", r.statusCode );
Is they a way to add some url parameters (like http://api.example.com/v3/object?data=123&info=test) to all restkit request witouth adding them manually to all
getObjectsAtPath:parameters:success:failure:
getObjectsAtPathForRouteNamed:object:parameters:success:failure:
...
each request should add the info parameter.
I've actually a way to do it, using Method Swizzling. Is they a way to do it directly with RestKit?
You have a couple of ways to do this:
you can either subclass the methods of RKObjectManager to something like this:
-(void)addedParamToGetObjectsAtPath:(NSString*)path parameters:(NSDictionary*)parameters success:(success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult)successBlock failure::^(RKObjectRequestOperation *operation, NSError *error){
NSMutableDictionary* newParams = [NSMutableDictionary new];
if(parameters){
[newParams addEntriesFromDictionary:parameters];
}
newParams[#"info"]=test;
getObjectsAtPath:(NSString*)path parameters:(NSDictionary*)parameters success:(success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult){
// Deal with the success here
successBlock(operation, mappingResult);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
//Deal with the error here
errorBlock(operation, error);
}];
Or tell Restkit to use a different RequestOperationClass
//When configuring RestKit
RKObjectManager *objectManager = [RKObjectManager managerWithBaseURL:[NSURL URLWithString:BASE_URL]];
//Some more configuration
//....
[objectManager registerRequestOperationClass:[YourObjectRequestOperation class]];
And define a subclass of RKObjectRequestOperation, YourObjectRequestOperation
#import "FBObjectRequestOperation.h"
#interface RKHTTPRequestOperation ()
#property (nonatomic, strong, readwrite) NSMutableURLRequest* request;
#end
#implementation FBObjectRequestOperation
- (id)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation responseDescriptors:(NSArray *)responseDescriptors
{
NSParameterAssert(requestOperation);
NSParameterAssert(responseDescriptors);
//your method to change the requestOperation
RKHTTPRequestOperation* myRequestOperation = [YourObjectRequestOperation addParametersToRequest:requestOperation];
self = [super initWithHTTPRequestOperation:myRequestOperation responseDescriptors:responseDescriptors];
if (self) {
//Change headers or any other thing that you need
}
return self;
}
To actually change the requestOperation you will need to get the url from the request and and add there the new parameters. That will happen in this part RKHTTPRequestOperation* myRequestOperation = [YourObjectRequestOperation addParametersToRequest:requestOperation]; and I am living up to you to complete the code.
This should work for any request you are doing with the object manager.
This technique is also very helpful is you need to calculate headers dynamically for each request.
I have the following code that works. It successfully displays myName in the NSLog ...
NSURL *apiString = [NSURL URLWithString:#"http://testapi.com/url"];
[XMLConverter convertXMLURL:apiString completion:^(BOOL success, NSDictionary *dictionary, NSError *error)
{
if (success)
{
NSString *myName = dictionary[#"profile"][#"real_name"];
NSLog(#"%# is my name", myName);
}
}];
I have the following code for the method convertXMLURL which is in XMLConverter.m which I imported. It does a nice job of converting my XML to NSDictionary. That is what I want ...
+ (void)convertXMLURL:(NSURL *)url completion:(OutputBlock)completion
{
///Wrapper for -initWithContentsOfURL: method of NSXMLParser
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[[XMLConverter new] parser:parser completion:completion];
});
}
The problem I have is *dictionary is a local variable. I need to use it elsewhere in the code. How can I return it?
Simply create a #property and assign dictionary to it inside your completion handler.
For example:
Class Foo
#interface Foo : NSObject
#property (strong, nonatomic) NSDictionary* dictionary;
#end
Completion handler
NSURL *apiString = [NSURL URLWithString:#"http://testapi.com/url"];
[XMLConverter convertXMLURL:apiString completion:^(BOOL success, NSDictionary *dictionary, NSError *error)
{
if (success)
{
NSString *myName = dictionary[#"profile"][#"real_name"];
NSLog(#"%# is my name", myName);
myInstanceOfFoo.dictionary = dictionary;
}
}];
EDIT: If the completion handler is not inside a member of the Foo class the dictionary property must be declared in the header file (.h). Otherwise you can declare it in the implementation file (.m).
Based on the comment by carlodurso
i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}
I'm writing some tests for a networking API with kiwi.
Here is the relevant code:
//////////////////////////////////////////////////////
MyAPI.h
//////////////////////////////////////////////////////
#protocol MyAPIDelegate<NSObject>
-(void) onMyMethodCall:(id)response;
-(void) onFail:(NSError*)error message:(NSString*)message;
#end
#interface MyAPI : NSObject
#property (nonatomic, weak) id<MyAPIDelegate> delegate;
-(void) myMethodCall;
#end
//////////////////////////////////////////////////////
MyAPI.m
//////////////////////////////////////////////////////
#synthesize delegate
-(void) myMethodCall
{
NSDictionary *params = [Dictionary dictionaryWithObject:#"value1" forKey:#"param1"];
[self apiCallWithServerPath:#"logic/myMethodCall"
parameters:params
onSuccess:^(id response) {
[delegate onMyMethodCall];
}
onFailure:^(NSError *error, NSString* msg) {
[delegate onFail:error message:msg];
}];
}
-(void) apiCallWithServerPath:(NSString *)serverPath parameters:(NSDictionary *)parameters onSuccess:(void (^)(id))success onFailure:(void (^)(NSError *, NSString *))failure
{
AFHTTPClient *client = [AFHTTPClient clientWithBaseURL:[NSURL urlWithString:#"http://www.myserver.com/"]];
[client setParameterEncoding:AFFormURLParameterEncoding];
[client postPath:serverPath
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject) {
id response = [[JSONDecoder decoder] objectWithData:responseObject];
success(response);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"API call failed: error message = %#",[error localizedDescription]);
failure(error, [error localizedRecoverySuggestion]);
}];
}
//////////////////////////////////////////////////////
MyAPITest.m
//////////////////////////////////////////////////////
SPEC_BEGIN(MyAPITest)
describe(#"MyAPITest should", ^{
__block MyAPI *api;
__block MyAPI *delegateMock;
beforeEach(^{
delegateMock = [KWMock mockForProtocol:#protocol(MyAPIDelegate)];
api = [[MyAPI alloc] init];
api.delegate = delegateMock;
});
afterEach(^{
delegateMock = nil;
api = nil;
});
it(#"should go to the server and get an answer, dont care about the value atm", ^{
[[api should] receive:#selector(myMethodCall)];
KWCaptureSpy *spy = [delegateMock captureArgument:#selector(onMyMethodCall:) atIndex:0];
// I usually put a breakpoint here...
[api myMethodCall];
[[expectFutureValue(spy.argument) shouldEventually] beNonNil];
});
});
SPEC_END
This is a very simple test, that I'm building incrementally. Eventually I'd like to test the values inside spy.argument, but for now i'm only interested in making sure that it is not nil.
Running the test always fails with: [FAILED] Argument requested has yet to be captured.
Debugging doesn't work: When I try to put breakpoint in the test (where the comment says) its never steps into the method.
At the same time, if I put NSLog(s) inside MyAPI's myMethodCall, they are not printed on console.
Any help would be much appreciated.
/////////////////////// UPDATE //////////////////////////////////
Turns out that if I comment/remove this line (from the test):
[[api should] receive:#selector(myMethodCall)];
The test works. Any idea of why this line is causing the problem?