I am now working on an app that works with BLE, Backend server and location. I am facing a problem which I am not sure how to get out of which is what people call "Callback hell". The entire CoreBluetooth framework in iOS is based on a delegate pattern, which until you can use the CBPeripheral has to go to at least 3 callbacks:
DidConnectToPeripheral
DidDiscoverServices
DidDiscoverCharacteristics
But in fact there could be many more, and every action you take with the device will come back as a callback to one of those functions. Now when I want to "Rent" this ble product, I must connect to it, after connecting send a requests to the server and get the user's current location, after that all happens I have to write a value in the bluetooth device and get confirmation. This would not be so difficult, but unfortunately each and every one of those stages is failable, so error handling needs to be added. Not to mention implementing timeout.
I am sure I am not the only one to approach such issues so I looked around and I found 2 things that might help:
the Advanced NSOperations talk in the wwdc 2015, but after trying for 4 days to make it work, it seems like the code is too buggy.
Promisekit but I couldn't find a way to wrap CoreBluetooth.
How are people with even more complicated apps deal with this? in swift or objc.
Some sample problematic code:
-(void)startRentalSessionWithLock:(DORLock *)lock timeOut:(NSTimeInterval)timeout forSuccess:(void (^)(DORRentalSession * session))successBlock failure:(failureBlock_t)failureBlock{
//we set the block to determine what happens
NSAssert(lock.peripheral, #"lock has to have peripheral to connect to");
if (!self.rentalSession) {
self.rentalSession = [[DORRentalSession alloc] initWithLock:nil andSessionDict:#{} active:NO];
}
self.rentalSession.lock = lock;
[self connectToLock:self.rentalSession.lock.peripheral timeOut:timeout completionBlock:^(CBPeripheral *peripheral, NSError *error) {
self.BTConnectionCompleted = nil;
if (!error) {
[[INTULocationManager sharedInstance] requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse timeout:1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) {
if (status == INTULocationStatusSuccess || status == INTULocationStatusTimedOut) {
[self startServerRentalForSessionLockWithUserLocation:currentLocation.coordinate forSuccess:^(DORRentalSession *session) {
if (self.rentalSession.lock.peripheral && self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
[self.rentalSession.lock.peripheral setNotifyValue:YES forCharacteristic:self.rentalSession.lock.charectaristics.sensorCharacteristic];
}else{
//shouldnt come here
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
!self.rentalSession.lock.open ? [self sendUnlockBLECommandToSessionLock] : nil;
if (successBlock) {
successBlock(session);
}
}else{
[self endCurrentRentalSessionWithLocation:self.rentalSession.lock.latLng andPositionAcc:#(1) Success:^(DORRentalSession *session) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:46 userInfo:#{NSLocalizedDescriptionKey:#"Could't connect to lock"}],200);
}
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:45 userInfo:#{NSLocalizedDescriptionKey:#"fatal error"}],200);
}
}];
}
});
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock(error,httpCode);
}
}];
}else{
NSError *gpsError = [self donkeyGPSErrorWithINTULocationStatus:status];
if (failureBlock) {
failureBlock(gpsError,200);
}
}
}];
}else{
if (failureBlock) {
failureBlock(error,200);
}
}
}];
}
To get rid of this nested calls you can use GCD group + serial execution queue:
dispatch_queue_t queue = ddispatch_queue_create("com.example.queue", NULL);
dispatch_group_t group = dispatch_group_create();
// Add a task to the group
dispatch_group_async(group, queue, ^{
// Some asynchronous work
});
// Make dispatch_group_async and dispatch_group_sync calls here
// Callback to be executed when all scheduled tasks are completed.
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
// Do smth when everything has finished
});
// wait for all tasks to complete
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
The other solution based on GCD groups is described here
Related
I'm trying to test Diary class that has dependency to Network.
So Diary code:
- (PMKPromise *)saveAndUploadToServer:(DiaryItem *)item
{
return [self save:item].then(^{
return [self upload:item]; << See UPDATE //I put breakpoint here, it is never called
});
}
- (PMKPromise *)save:(DiaryItem *)item
{
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[self.entryCreationManagedContext performBlock:^{
BOOL success;
NSError *saveError = nil;
item.status = #(UploadingStatus);
success = [self.entryCreationManagedContext save:&saveError];
if (success) {
fulfill(item.objectID);
}
else {
reject(saveError);
}
}];
}];
}
- (PMKPromise*)upload:(DiaryItem*)item
{
return [self.network POST:self.diaryUrl parameters:[item dictionary]].then(^{
return [self reportUploadAnalytics];
});
}
And the test:
- (void)testFailedUploadingReportsAnalytics
{
XCTestExpectation *expectation = [self expectationWithDescription:#"Operations completed"];
[self uploadToServerAndReturnCallback].finally(^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
assertThat(error, is(nilValue()));
//check that mock called
}];
}
The Network is mock in this test. But what I see that chain of promises is not executed. It stuck. Maybe because then: block is called on main thread as well XCTest is pausing it. But at the same time it should probably continue after 5 sec. What can be the issue?
UPDATE
Looks like it is nothing with my original assumption. If I replace [self.entryCreationManagedContext save:&saveError] with YES then debug reaches breakpoint.
UPDATE 2
It looks like issue with this particular saving of managed context. It is triggering notification about synchronising another managed contexts. And we are discovering what else there.
It is ended up in different issue and nothing connected to PromiseKit.
We discovered growing amount of used memory. That NSManagedObjectContextDidSaveNotification was producing deadlock on different store coordinators. After fixing that tests started working as expected.
I am looking for a small scenario that how can we trace the "dispatch_async" is running or not?.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
//back ground process
});
In my case, my app will be in foreground I started the back ground thread and when I bring app from background to foreground I need to check whether it is still running or not. I should not call the same process if it is still running. any idea?
The easiest way to do this (without keeping a reference to every dispatch or a flag for entering/leaving asynchronous tasks) is by using dispatch_group notifications. See the example link and code below:
- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{
// 1
__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();
for (NSInteger i = 0; i < 3; i++) {
NSURL *url;
switch (i) {
case 0:
url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
break;
case 1:
url = [NSURL URLWithString:kSuccessKidURLString];
break;
case 2:
url = [NSURL URLWithString:kLotsOfFacesURLString];
break;
default:
break;
}
dispatch_group_enter(downloadGroup); // 2
Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *_error) {
if (_error) {
error = _error;
}
dispatch_group_leave(downloadGroup); // 3
}];
[[PhotoManager sharedManager] addPhoto:photo];
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
if (completionBlock) {
completionBlock(error);
}
});
}
Note how:
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4
if (completionBlock) {
completionBlock(error);
}
});
will not be called until after
dispatch_group_leave(downloadGroup); // 3
is called.
You should setup your threading to where you can work with callbacks like this to determine states. You should try to avoid using boolean flags at all costs, as this is exactly what dispatch groups are for. It's also hard to keep track of numerous asynchronous calls using boolean states.
link: dispatch groups
The question is wrong - dispatch_async is running while you call it and stops running when the call returns, which is practically immediately. What you really want to know is whether the dispatched block is running or not. The simplest way is something along the lines of
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
[self blockIsRunning:YES];
// do stuff
[self blockIsRunning:NO];
});
or if you want to know whether the block has run once, you would do something like
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
(unsigned long)NULL), ^(void) {
[self blockStarted];
// do stuff
[self blockFinished];
});
Alternatively, use NSOperationQueue and a subclass of NSOperation so instead of an anonymous block you have a proper object that you can ask whether it is ready, cancelled, executing, or finished.
If I have an array of Message objects, each with a PFile containing data, is it possible to download the data for every single message by queuing them up asynchronously like so:
for (int i = 0; i < _downloadedMessages.count; i++) {
PFObject *tempMessage = (PFObject *)[_downloadedMessages objectAtIndex:i];
[[tempMessage objectForKey:#"audio"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[self persistNewMessageWithData:data];
}];
}
This seems to cause my app to hang, even though this should be done in the background...
Using the solution below:
NSMutableArray* Objects = ...
[self forEachPFFileInArray:Objects retrieveDataWithCompletion:^BOOL(NSData* data, NSError*error){
if (data) {
PFObject *tempObj = (PFObject *)Object[someIndex...];
[self persistNewMessageWithData:data andOtherInformationFromObject:tempObj];
return YES;
}
else {
NSLog(#"Error: %#", error);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];
What you are possibly experiencing is, that for a large numbers of asynchronous requests which run concurrently, the system can choke due to memory pressure and due to network stalls or other accesses of resources that get exhausted (including CPU).
You can verify the occurrence of memory pressure using Instruments with the "Allocations" tool.
Internally (that is, in the Parse library and the system) there might be a variable set which sets the maximum number of network requests which can run concurrently. Nonetheless, in your for loop you enqueue ALL requests.
Depending of what enqueuing a request means in your case, this procedure isn't free at all. It may cost a significant amount of memory. In the worst case, the network request will be enqueued by the system, but the underlying network stack executes only a maximum number of concurrent requests. The other enqueued but pending requests hang there and wait for execution, while their network timeout is already running. This may lead to cancellation of pending events, since their timeout expired.
The simplest Solution
Well, the most obvious approach solving the above issues would be one which simply serializes all tasks. That is, it only starts the next asynchronous task when the previous has been finished (including the code in your completion handler). One can accomplish this using an asynchronous pattern which I name "asynchronous loop":
The "asynchronous loop" is asynchronous, and thus has a completion handler, which gets called when all iterations are finished.
typedef void (^loop_completion_handler_t)(id result);
typedef BOOL (^task_completion_t)(PFObject* object, NSData* data, NSError* error);
- (void) forEachObjectInArray:(NSMutableArray*) array
retrieveDataWithCompletion:(task_completion_t)taskCompletionHandler
completion:(loop_completion_handler_t)completionHandler
{
// first, check termination condition:
if ([array count] == 0) {
if (completionHandler) {
completionHandler(#"Finished");
}
return;
}
// handle current item:
PFObject* object = array[0];
[array removeObjectAtIndex:0];
PFFile* file = [object objectForKey:#"audio"];
if (file==nil) {
if (taskCompletionHandler) {
NSDictionary* userInfo = #{NSLocalizedFailureReasonErrorKey: #"file object is nil"}
NSError* error = [[NSError alloc] initWithDomain:#"RetrieveObject"
code:-1
userInfo:userInfo];
if (taskCompletionHandler(object, nil, error)) {
// dispatch asynchronously, thus invoking itself is not a recursion
dispatch_async(dispatch_get_global(0,0), ^{
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
});
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}
}
else {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
BOOL doContinue = YES;
if (taskCompletionHandler) {
doContinue = taskCompletionHandler(object, data, error);
}
if (doContinue) {
// invoke itself (note this is not a recursion")
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}];
}
}
Usage:
// Create a mutable array
NSMutableArray* objects = [_downloadedMessages mutableCopy];
[self forEachObjectInArray:objects
retrieveDataWithCompletion:^BOOL(PFObject* object, NSData* data, NSError* error){
if (error == nil) {
[self persistNewMessageWithData:data andOtherInformationFromObject:object];
return YES;
}
else {
NSLog(#"Error %#\nfor PFObject %# with data: %#", error, object, data);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];
GCD and blocks are so nice and convenient. But when I fall in love with it, I found that something bad happened. Look at these codes below:
[self functionA:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self functionB:^(NSError *error) {
if (error != nil) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self functionC:^(id result) {
if (result) {
[self functionD:^(BOOL success) {
if (success) {
[self DoSomething];
}
}];
}
}];
});
}
}];
});
}
}];
Crazy? Yes. I am in this trouble.
Did someone have any experience avoiding nested blocks like this?
Edited:
Thanks guys. Exactly, we have more elegant ways to do this. Such as:
Declare blocks beforehand
Make sub blocks as independent function
But what I expect is a general solution. Maybe like this:(Pseudo code below)
functionA.flat.success = [self functionB];
functionB.flat.isntnil = [self functionC];
functionB.flat.error = {};
functionC.flat.isntnil = [self functionD];
[flat call:functionA];
Well, I haven't bothered matching your cloud of closing braces, but here's a try by simply using return, which you can use freely inside blocks too and cuts the nesting a little:
[self functionA:^(BOOL success) {
if (!success)
return;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[self functionB:^(NSError *error) {
if (!error)
return;
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self functionC:^(id result) {
if (!result)
return;
[self functionD:^(BOOL success) {
if (!success)
return;
[self DoSomething];
}];
}];
});
}];
});
}];
Also, nobody forces you to write blocks inline, you can declare them as normal variables before and use them later. In fact, by declaring blocks before you are able to reuse them if your API is lenient towards its users and allows being called repeatedly even when no work has to be done:
- (void)foo:(Bar*)bar
{
// Prepare the success handler.
void (^successBlock)(Bar*) = ^(Bar *bar) {
[[NSNotificationCenter defaultCenter]
postNotificationName:#"barUpdated"
object:bar];
};
if (!bar.didAlreadyFetchStuff) {
[self wellYouBetterFetchSomething:bar withSuccess:successBlock];
} else {
// Oh, fake we already did the work.
successBlock(bar);
}
}
Whenever I see the nest level too high I put the inside blocks as normal methods in the class and simply call them inside the block. The effect is the same, but it looks much cleaner, and it allows you to use appledoc or other documentation tools for each method rather than hoping to understand the mess of nested undocumented blocks.
It only gets crazy if you allow it to get crazy.
I am facing a problem while unit testing an asynchronous call in iOS. (Although it is working fine in view controllers.)
Has anyone faced this issue before? I have tried using a wait function but I'm still facing the same problem.
Please suggest an example of a good way to do this.
You'll need to spin the runloop until your callback is invoked. Make sure that it gets invoked on the main queue, though.
Try this:
__block BOOL done = NO;
doSomethingAsynchronouslyWithBlock(^{
done = YES;
});
while(!done) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
You can also use a semaphore (example below), but I prefer to spin the runloop to allow asynchronous blocks dispatched to the main queue to be processed.
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
doSomethingAsynchronouslyWithBlock(^{
//...
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
Here is Apple's description of native support for async testing.
TL;DR manual:
Look at XCTextCase+AsynchronousTesting.h
There is special class XCTestExpectation with only one public method: - (void)fulfill;
You should init instance of this class and in success case call fulfill method. Otherwise your test will fail after timeout that you specify in that method:
- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(XCWaitCompletionHandler)handlerOrNil;
Example:
- (void)testAsyncMethod
{
//Expectation
XCTestExpectation *expectation = [self expectationWithDescription:#"Testing Async Method Works Correctly!"];
[MyClass asyncMethodWithCompletionBlock:^(NSError *error) {
if(error)
NSLog(#"error is: %#", error);
else
[expectation fulfill];
}];
//Wait 1 second for fulfill method called, otherwise fail:
[self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
if(error)
{
XCTFail(#"Expectation Failed with error: %#", error);
}
}];
}
I think many of the suggested solutions in this post has the problem that if the asynchronous operation does not complete the "done" flag is never set, and the test will hang forever.
I have successfully used this approach in many of my test.
- (void)testSomething {
__block BOOL done = NO;
[obj asyncMethodUnderTestWithCompletionBlock:^{
done = YES;
}];
XCTAssertTrue([self waitFor:&done timeout:2],
#"Timed out waiting for response asynch method completion");
}
- (BOOL)waitFor:(BOOL *)flag timeout:(NSTimeInterval)timeoutSecs {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if ([timeoutDate timeIntervalSinceNow] < 0.0) {
break;
}
}
while (!*flag);
return *flag;
}
Since Xcode 6 this built in to XCTest as a category:
See https://stackoverflow.com/a/24705283/88164
Here's another alternative, XCAsyncTestCase, that works well with OCMock if you need to use it. It's based on GHUnit's async tester, but is uses the regular XCTest framework instead.
Fully compatible with Xcode Bots.
https://github.com/iheartradio/xctest-additions
Usage is the same, just import and subclass XCAsyncTestCase.
#implementation TestAsync
- (void)testBlockSample
{
[self prepare];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
sleep(1.0);
[self notify:kXCTUnitWaitStatusSuccess];
});
// Will wait for 2 seconds before expecting the test to have status success
// Potential statuses are:
// kXCTUnitWaitStatusUnknown, initial status
// kXCTUnitWaitStatusSuccess, indicates a successful callback
// kXCTUnitWaitStatusFailure, indicates a failed callback, e.g login operation failed
// kXCTUnitWaitStatusCancelled, indicates the operation was cancelled
[self waitForStatus:kXCTUnitWaitStatusSuccess timeout:2.0];
}
AGAsyncTestHelper is a C macro for writing unit tests with asynchronous operations and works with both SenTestingKit and XCTest.
Simple and to the point
- (void)testAsyncBlockCallback
{
__block BOOL jobDone = NO;
[Manager doSomeOperationOnDone:^(id data) {
jobDone = YES;
}];
WAIT_WHILE(!jobDone, 2.0);
}
Sam Brodkin already gave the right answer.
Just to make the answer looks better at first sight, I bring the sample code here.
Use XCTestExpectation.
// Test that the document is opened. Because opening is asynchronous,
// use XCTestCase's asynchronous APIs to wait until the document has
// finished opening.
- (void)testDocumentOpening
{
// Create an expectation object.
// This test only has one, but it's possible to wait on multiple expectations.
XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:#"document open"];
NSURL *URL = [[NSBundle bundleForClass:[self class]]
URLForResource:#"TestDocument" withExtension:#"mydoc"];
UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
[doc openWithCompletionHandler:^(BOOL success) {
XCTAssert(success);
// Possibly assert other things here about the document after it has opened...
// Fulfill the expectation-this will cause -waitForExpectation
// to invoke its completion handler and then return.
[documentOpenExpectation fulfill];
}];
// The test will pause here, running the run loop, until the timeout is hit
// or all expectations are fulfilled.
[self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
[doc closeWithCompletionHandler:nil];
}];
}
you can use async api calling in swift like this
private let serverCommunicationManager : ServerCommunicationManager = {
let instance = ServerCommunicationManager()
return instance
}()
var expectation:XCTestExpectation?
func testAsyncApiCall() {
expectation = self.expectation(description: "async request")
let header = ["Authorization":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImQ4MmY1MTcxNzI4YTA5MjI3NWIzYWI3OWNkOTZjMGExOTI4MmM2NDEyZjMyYWQzM2ZjMzY4NmU2MjlhOWY2YWY1NGE0MDI4MmZiNzY2NWQ3In0.eyJhdWQiOiIxIiwianRpIjoiZDgyZjUxNzE3MjhhMDkyMjc1YjNhYjc5Y2Q5NmMwYTE5MjgyYzY0MTJmMzJhZDMzZmMzNjg2ZTYyOWE5ZjZhZjU0YTQwMjgyZmI3NjY1ZDciLCJpYXQiOjE1MDg4MjU1NTEsIm5iZiI6MTUwODgyNTU1MSwiZXhwIjoxNTQwMzYxNTUxLCJzdWIiOiIiLCJzY29wZXMiOltdfQ.osoMQgiY7TY7fFrh5r9JRQLQ6AZhIuEbrIvghF0VH4wmkqRUE6oZWjE5l0jx1ZpXsaYUhci6EDngnSTqs1tZwFTQ3srWxdXns2R1hRWUFkAN0ri32W0apywY6BrahdtiVZa9LQloD1VRMT1_QUnljMXKsLX36gXUsNGU6Bov689-bCbugK6RC3n4LjFRqJ3zD9gvkRaODuOQkqsNlS50b5tLm8AD5aIB4jYv3WQ4-1L74xXU0ZyBTAsLs8LOwvLB_2B9Qdm8XMP118h7A_ddLo9Cyw-WqiCZzeZPNcCvjymNK8cfli5_LZBOyjZT06v8mMqg3zszWzP6jOxuL9H1JjBF7WrPpz23m7dhEwa0a-t3q05tc1RQRUb16W1WhbRJi1ufdMa29uyhX8w_f4fmWdAnBeHZ960kjCss98FA73o0JP5F0GVsHbyCMO-0GOHxow3-BqyPOsmcDrI4ay006fd-TJk52Gol0GteDgdntvTMIrMCdG2jw8rfosV6BgoJAeRbqvvCpJ4OTj6DwQnV-diKoaHdQ8vHKe-4X7hbYn_Bdfl52gMdteb3_ielcVXIaHmQ-Dw3E2LSVt_cSt4tAHy3OCd7WORDY8uek4Paw8Pof0OiuqQ0EB40xX5hlYqZ7P_tXpm-W-8ucrIIxgpZb0uh-wC3EzBGPjpPD2j9CDo"]
serverCommunicationManager.sendServerRequest(httpMethodType: .get, baseURL: "http://192.168.2.132:8000/api/v1/user-role-by-company-id/2", param: nil, header: header) { (isSuccess, msg , response) in
if isSuccess
{
let array = response as! NSArray
if array.count == 8
{
XCTAssertTrue(true)
self.expectation?.fulfill()
}
else
{
XCTAssertFalse(false)
XCTFail("array count fail")
}
}
}
waitForExpectations(timeout: 5) { (error) in
if let error = error{
XCTFail("waiting with error: \(error.localizedDescription)")
}
}
}
I suggest you should have a look on the tests of Facebook-ios-sdk. It's a good example of how to test async unit test on iOS, though personally I think async tests should be break into sync tests.
FBTestBlocker: a blocker that prevent current thread exits with specified timeout. You can drag and drop this to your project, but you need to remove OCMock related stuff if you don't have that in you project.
FBTestBlocker.h
FBTestBlocker.m
FBURLConnectionTests: test examples you should look at.
FBURLConnectionTests.h
FBURLConnectionTests.m
This code snippet should give you some idea
- (void)testExample
{
FBTestBlocker *_blocker = [[FBTestBlocker alloc] initWithExpectedSignalCount:1];
__block BOOL excuted = NO;
[testcase test:^(BOOL testResult) {
XCTAssert(testResult, #"Should be true");
excuted = YES;
[_blocker signal];
}];
[_blocker waitWithTimeout:4];
XCTAssertTrue(excuted, #"Not executed");
}
Try KIWI framework. It's powerful and might help you with other kinds of tests.
I recommend you connection semaphore + runloop, i also wrote method which take block:
// Set the flag to stop the loop
#define FLEND() dispatch_semaphore_signal(semaphore);
// Wait and loop until flag is set
#define FLWAIT() WAITWHILE(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
// Macro - Wait for condition to be NO/false in blocks and asynchronous calls
#define WAITWHILE(condition) \
do { \
while(condition) { \
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]; \
} \
} while(0)
method:
typedef void(^FLTestAsynchronousBlock)(void(^completion)(void));
void FLTestAsynchronous(FLTestAsynchronousBlock block) {
FLSTART();
block(^{
FLEND();
});
FLWAIT();
};
and call
FLTestAsynchronous(^(void(^completion)()){
[networkManager signOutUser:^{
expect(networkManager.currentUser).to.beNil();
completion();
} errorBlock:^(NSError *error) {
expect(networkManager.currentUser).to.beNil();
completion();
}];
});
If you are using XCode 6, you can test async network calls like this:
XCTest and asynchronous testing in Xcode 6