Memory leak when making NSURLSession calls and loading images to NSImage - ios

I've built a small demo-application which allows the user to choose a color, which is sent to a basic (for now localhost) node.js server (using NSURLSessionDataTask), which uses the color name to get a fruit name and image URL, and return a simple 2 property JSON object containing the two.
When the application receives the JSON response, it creates a sentence with the color name and fruit name to display in the GUI, and then spawns another NSURLSession call (this time using NSURLSessionDownloadTask) to consume the image URL and download a picture of the fruit to also display in the GUI.
Both of these network operations use [NSURLSession sharedSession].
I'm noticing that both the JSON call and more noticeably the image download are leaking significant amounts of memory. They each follow a similar pattern using nested blocks:
Initialize the session task, passing a block as the completion handler.
If I understand correctly, the block is run on a separate thread since the communication in NSURLSession is async by default, so updating the GUI has to happen in the main, so within the completeHandler block, a call to dispatch_async is made, specifying the main thread, and a short nested block that makes a call to update the GUI.
My guess is that either my use of nested blocks, or nesting of GCD calls is causing the issue. Though it's entirely possible my problem is multi-faceted.
Was hoping some of you with more intimate knowledge of how Obj-C manages memory with threads and ARC would be greatly helpful. Relevant code is included below:
AppDelegate.m
#import "AppDelegate.h"
#import "ColorButton.h"
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (weak) IBOutlet NSImageView *fruitDisplay;
#property (weak) IBOutlet NSTextField *fruitNameLabel;
#property (weak) IBOutlet ColorButton *redButton;
#property (weak) IBOutlet ColorButton *orangeButton;
#property (weak) IBOutlet ColorButton *yellowButton;
#property (weak) IBOutlet ColorButton *greenButton;
#property (weak) IBOutlet ColorButton *blueButton;
#property (weak) IBOutlet ColorButton *purpleButton;
#property (weak) IBOutlet ColorButton *brownButton;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
proxy = [[FruitProxy alloc] init];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
// Insert code here to tear down your application
}
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
return YES;
}
/*------------------------------------------------------------------*/
- (IBAction)colorButtonWasClicked:(id)sender
{
ColorButton *btn = (ColorButton*)sender;
NSString *selectedColorName = btn.colorName;
#autoreleasepool {
[proxy requestFruitByColorName:selectedColorName
completionResponder:^(NSString* fruitMessage, NSString* imageURL)
{
[self fruitNameLabel].stringValue = fruitMessage;
__block NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
__block NSURLSession *imageSession = [NSURLSession sharedSession];
__block NSURLSessionDownloadTask *imgTask = [imageSession downloadTaskWithRequest:req
completionHandler:
^(NSURL *location, NSURLResponse *response, NSError *error)
{
if(fruitImage != nil)
{
[self.fruitDisplay setImage:nil];
fruitImage = nil;
}
req = nil;
imageSession = nil;
imgTask = nil;
response = nil;
fruitImage = [[NSImage alloc] initWithContentsOfURL:location];
[fruitImage setCacheMode:NO];
dispatch_async
(
dispatch_get_main_queue(),
^{
[[self fruitDisplay] setImage: fruitImage];
}
);
}];
[imgTask resume];
}];
}
}
#end
FruitProxy.m
#import "FruitProxy.h"
#implementation FruitProxy
- (id)init
{
self = [super init];
if(self)
{
return self;
}
else
{
return nil;
}
}
- (void) requestFruitByColorName:(NSString*)colorName
completionResponder:(void( ^ )(NSString*, NSString*))responder
{
NSString *requestURL = [self urlFromColorName:colorName];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:requestURL]];
session = [NSURLSession sharedSession];
#autoreleasepool {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:
^(NSData *data, NSURLResponse *response, NSError *connectionError)
{
NSString *text = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSDictionary *responseObj = (NSDictionary*)[NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSString *fruitName = (NSString*)responseObj[#"fruitName"];
NSString *imageURL = (NSString*)responseObj[#"imageURL"];
NSLog(#"Data = %#",text);
dispatch_async
(
dispatch_get_main_queue(),
^{
responder([self messageFromColorName:colorName fruitName:fruitName], imageURL);
}
);
}];
[task resume];
}
}
- (NSString*)urlFromColorName:(NSString*)colorName
{
NSString *result;
result = #"http://localhost:9000/?color=";
result = [result stringByAppendingString:colorName];
return result;
}
- (NSString*)messageFromColorName:(NSString*)colorName
fruitName:(NSString*)fruitName
{
NSString *result = #"A ";
result = [[[[result stringByAppendingString:colorName]
stringByAppendingString:#"-colored fruit could be "]
stringByAppendingString:fruitName]
stringByAppendingString:#"!"];
return result;
}
#end

Where does "fruitImage" come from in AppDelegate.m? I don't see it declared.
the line:
__block NSURLSessionDownloadTask *imgTask
is a bit weird because you're marking imgTask as a reference that can change in the block, but it's also the return value. That might be part of your problem, but in the very least it's unclear. I might argue that all the variables you marked __block aren't required to be as such.
typically a memory leak in these situations is caused by the variable capture aspect of the block, but I'm not seeing an obvious offender. The "Weak Self" pattern might help you here.
Using "leaks" might help you see what objects are leaking, which can help isolate what to focus on, but also try to take a look at your block's life cycles. If a block is being held by an object it can create cycles by implicitly retaining other objects.
Please follow up when you figure out exactly what's going on.
reference:
What does the "__block" keyword mean?
Always pass weak reference of self into block in ARC?

Related

Load Data to UILabels and UITextFields after getting data from a json

I'm new to iOS development and to stack overflow. I need to show user's data in multiple UILabel and UITextField, the data is obtained from a POST method. There is a slight delay for getting the data from the server. How to reload or populate the elements after getting the details.
This is my viewDidLoad()
#interface EditProfileViewController ()
#property (weak, nonatomic) IBOutlet UIImageView *displayPictureView;
#property (weak, nonatomic) IBOutlet UITextField *firstnameFeld;
#property (weak, nonatomic) IBOutlet UITextField *lastNameField;
#property (weak, nonatomic) IBOutlet UITextField *emailField;
#property (weak, nonatomic) IBOutlet UITextField *bdayField;
#property (weak, nonatomic) IBOutlet UIButton *calField;
#property (weak, nonatomic) IBOutlet UITextView *addressTextView;
#property (weak, nonatomic) IBOutlet UILabel *userIDLabel;
#property (weak, nonatomic) IBOutlet UILabel *phoneNumberLabel;
#property (weak, nonatomic) IBOutlet UIScrollView *editScrollView;
#end
#implementation EditProfileViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.editScrollView.delegate=self;
}
I receive data from the server using the code, i have this POST method in viewDidLoad()
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
NSMutableURLRequest * urlRequest = [NSMutableURLRequest requestWithURL:urlEdit];
[urlRequest setHTTPMethod:#"POST"];
[urlRequest setHTTPBody:[editParameters dataUsingEncoding:NSUTF8StringEncoding]];
NSURLSessionDataTask * dataTask =[defaultSession dataTaskWithRequest:urlRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Response:%# \nError:%#\n", response, error);
if(error == nil)
{
NSString * text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"DataText = %#",text);
}
NSError *error2 = nil;
jsonDicEditAcc = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error2];
if (error2 != nil) {
NSLog(#"Error parsing JSON.");
}
else {
NSLog(#"JsonDictEditAcc: \n%#",jsonDicEditAcc);
[self dismissViewControllerAnimated:alert completion:nil];}
}];
[dataTask resume];
I will extract data from the "jsonDicEditAcc", i need to know how to populate the UILabels and UITextField, after getting the data because the view gets loaded before getting the data. I have tried viewWillAppear() did not work. Help me out.. Thanks in advance..
You'll need to know the content of the data you're trying to apply. If you know the key's that associate with each value you can apply those values to the text fields (and labels and buttons) and be done. They will change when you successfully update them.
Inside your block you need to trigger the population of the text fields. You can add something like this:
else {
NSLog(#"JsonDictEditAcc: \n%#",jsonDicEditAcc);
[self updateLabelsAndTextFields];
[self dismissViewControllerAnimated:alert completion:nil];}
}];
This function that will extract the data from jsonDicEditAcc and update the text of each relevant UILabel. The view has already loaded, but the UILabel will update when you change the text.
A solution could look like this:
- (void)updateLabelsAndTextFields{
self.firstnameFeld.text = jsonDicEditAcc[#"key"];
self.lastNameField.text = jsonDicEditAcc[#"key"];
self.emailField.text = jsonDicEditAcc[#"key"];
self.bdayField.text = jsonDicEditAcc[#"key"];
self.userIDLabel.text = jsonDicEditAcc[#"key"];
[calField setTitle:jsonDicEditAcc[#"key"] forState:UIControlStateNormal];
}
Be sure name a UIButton a button, i.e. "calButton" not "calField"
It's pretty straightforward, add similar code to the else branch of your if (error2 != nil) statement:
weakSelf.firstnameFeld = jsonDicEditAcc[#"<key-for-firstname>"];
Also, you might find it necessary to remove this statement from that branch as it will close the current viewController:
[self dismissViewControllerAnimated:alert completion:nil];
And I might point out, you need to use weakSelf inside of blocks to prevent memory leaks. Using self inside a block creates a retain cycle which means your entire viewController remains allocated until iOS eventually crashes your app for excessive memory use.
Put this at the top of your method and it is accessible inside the block, and then change all references of self inside of blocks to weakSelf.
typeof(self) __weak weakSelf = self;

Objective C - How to return local variable in this code?

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

UITableView is being reset when I use Custom Class

I have a UITableView with 2 text field and a button. If I run the simulator without use custom class, I can see the text fields and button:
But when i use a custom class, my UITable view only display a lot of lines without content:
Here is how I've created my properties:
LoginSceneController.h
#import <UIKit/UIKit.h>
#interface LoginSceneController : UITableViewController
#property (nonatomic, strong) IBOutlet UITextField *email;
#property (nonatomic, strong) IBOutlet UITextField *password;
- (IBAction)doLogin;
#end
LoginSceneController.m
#import "LoginSceneController.h"
#interface LoginSceneController ()
#end
#implementation LoginSceneController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)doLogin {
NSURL *url = [NSURL URLWithString:#"http://rest-service.guides.spring.io/greeting"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data, NSError *connectionError)
{
if (data.length > 0 && connectionError == nil)
{
NSDictionary *greeting = [NSJSONSerialization JSONObjectWithData:data
options:0
error:NULL];
self.email.text = [[greeting objectForKey:#"id"] stringValue];
self.password.text = [greeting objectForKey:#"content"];
}
}];
}
#end
The problem happens when I use a custom class (or referencing outlet or add a send event on button).
What is wrong?
edit: I think that I need populate my interface using my custom class because the static content is being lost. Is it possible to be the cause of content being lost?
You have two options when it comes to UITableView and Interface Builder. You can have a dynamic table view (pretty common) where your code overrides UITableViewController methods like "numberOfRowsInSection" and "cellForRowAtIndexPath". The other option is a static tableview, and that seems like what you want to do (especially since you haven't overridden the two aforementioned methods, and leads to your blank table). My guess is you need to select "static" for the tableview as shown in the third screenshot in this tutorial.

Best practices for making a queue of NSURLSessionTasks

What are the best practices for making a serial queue of NSURLSessionTasks ?
In my case, I need to:
Fetch a URL inside a JSON file (NSURLSessionDataTask)
Download the file at that URL (NSURLSessionDownloadTask)
Here’s what I have so far:
session = [NSURLSession sharedSession];
//Download the JSON:
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task =
[session dataTaskWithRequest:dataRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Figure out the URL of the file I want to download:
NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:#"download_url"]];
NSURLSessionDownloadTask *fileDownloadTask =
[session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(#"completed!");
}];
[fileDownloadTask resume];
}
];
Apart from the fact that writing a completion block within another completion looks messy, I am getting an EXC_BAD_ACCESS error when I call [fileDownloadTask resume]... Even though fileDownloadTask is not nil!
So, what is the best of way of sequencing NSURLSessionTasks?
You need to use this approach which is the most straight forward: https://stackoverflow.com/a/31386206/2308258
Or use an operation queue and make the tasks dependent on each others
=======================================================================
Regarding the HTTPMaximumConnectionsPerHost method
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1
HTTPMaximumConnectionsPerHost only ensure that one shared connection will be used for the tasks of that session but it does not mean that they will be processed serially.
You can verify that on the network level using http://www.charlesproxy.com/, you wil discover that when setting HTTPMaximumConnectionsPerHost, your tasks will be still be started together at the same time by NSURLSession and not serially as believed.
Expriment 1:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
With task1: url = download.thinkbroadband.com/20MB.zip
With task2: url = download.thinkbroadband.com/20MB.zip
calling [task1 resume];
calling [task2 resume];
Result: task1 completionBlock is called then task2 completionBlock is called
The completion blocks might be called in the order you expected in case the tasks take the same amount of time however if you try to download two different thing using the same NSURLSession you will discover that NSURLSession does not have any underlying ordering of your calls but only completes whatever finishes first.
Expriment 2:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
task1: url = download.thinkbroadband.com/20MB.zip
task2: url = download.thinkbroadband.com/10MB.zip (smaller file)
calling [task1 resume];
calling [task2 resume];
Result: task2 completionBlock is called then task1 completionBlock is called
In conclusion you need to do the ordering yourself, NSURLSession does not have any logic about ordering requests it will just call the completionBlock of whatever finishes first even when setting the maximum number of connections per host to 1
PS: Sorry for the format of the post I do not have enough reputation to post screenshots.
Edit:
As mataejoon has pointed out, setting HTTPMaximumConnectionsPerHost to 1 will not guarantee that the connections are processed serially. Try a different approach (as in my original answer bellow) if you need a reliable serial queue of NSURLSessionTask.
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1:
+ (NSURLSession *)session
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPMaximumConnectionsPerHost:1];
session = [NSURLSession sessionWithConfiguration:configuration];
});
return session;
}
then add tasks to it in the order you want.
NSURLSessionDataTask *sizeTask =
[[[self class] session] dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#import "SessionTaskQueue.h"
#interface SessionTaskQueue ()
#property (nonatomic, strong) NSMutableArray * sessionTasks;
#property (nonatomic, strong) NSURLSessionTask * currentTask;
#end
#implementation SessionTaskQueue
- (instancetype)init {
self = [super init];
if (self) {
self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];
}
return self;
}
- (void)addSessionTask:(NSURLSessionTask *)sessionTask {
[self.sessionTasks addObject:sessionTask];
[self resume];
}
// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {
self.currentTask = nil;
[self resume];
}
- (void)resume {
if (self.currentTask) {
return;
}
self.currentTask = [self.sessionTasks firstObject];
if (self.currentTask) {
[self.sessionTasks removeObjectAtIndex:0];
[self.currentTask resume];
}
}
#end
and use like this
__block __weak NSURLSessionTask * wsessionTask;
use_wself();
wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
use_sself();
[self.sessionTaskQueue sessionTaskFinished:wsessionTask];
...
}];
[self.sessionTaskQueue addSessionTask:wsessionTask];
I use NSOperationQueue (as Owen has suggested). Put the NSURLSessionTasks in NSOperation subclasses and set any dependancies. Dependent tasks will wait until the task they are dependent on is completed before running but will not check the status (success or failure) so add some logic to control the process.
In my case, the first task checks if the user has a valid account and creates one if necessary. In the first task I update a NSUserDefault value to indicate the account is valid (or there is an error). The second task checks the NSUserDefault value and if all OK uses the user credentials to post some data to the server.
(Sticking the NSURLSessionTasks in separate NSOperation subclasses also made my code easier to navigate)
Add the NSOperation subclasses to the NSOperationQueue and set any dependencies:
NSOperationQueue *ftfQueue = [NSOperationQueue new];
FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
[createFTFAccount setUserid:#"********"]; // Userid to be checked / created
[ftfQueue addOperation:createFTFAccount];
FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
[postFTFRoute addDependency:createFTFAccount];
[ftfQueue addOperation:postFTFRoute];
In the first NSOperation subclass checks if account exists on server:
#implementation FTFCreateAccount
{
NSString *_accountCreationStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"CHECKING" forKey:ftfAccountStatusKey];
// Setup and Run the NSURLSessionTask
[self createFTFAccount:[self userid]];
// Hold it here until the SessionTask completion handler updates the _accountCreationStatus
// Or the process takes too long (possible connection error)
while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
}
if ([_accountCreationStatus isEqualToString:#"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];
if ([self isCancelled]) {
NSLog(#"DEBUG FTFCreateAccount Cancelled" );
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"ERROR" forKey:ftfAccountStatusKey];
}
}
In the next NSOperation post data:
#implementation FTFPostRoute
{
NSString *_routePostStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
if ([ftfAccountStatus isEqualToString:#"ERROR"])
{
// There was a ERROR in creating / accessing the user account. Cancel the post
[self cancel];
} else
{
// Call method to setup and run json post
// Hold it here until a reply comes back from the operation
while ((!_routePostStatus) && (timeElapsed < 3)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
NSLog(#"FTFPostRoute time elapsed: %f", timeElapsed);
}
}
if ([self isCancelled]) {
NSLog(#"FTFPostRoute operation cancelled");
}
}

how to unit testing AFNetworking request

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.

Resources