Basically I have an existing API manager that is blocking me from going forward. This existing manager is something I should not mess with right now.
This is the gist of my problem. I mean, I can go forward with this way, but it's sooooo annoying. I hate these nested completion blocks. Does anyone have any workaround idea that can solve this? PromiseKit is out of the option.
This is how I call it.
- (void)doEverythingHere {
[self getDataOneWithCompletion:^(DataOneModel *response) {
if (response.isSomething) {
[self getDataTwoWithCompletion:^(DataOneModel *response) {
// And call some more of these...
// So the nested blocks will never end... and it's ugly
};
} else {
[self getDataThreeWithCompletion:^(DataOneModel *response) {
// And call some more of these...
// So the nested blocks will never end... and it's ugly
};
}
}];
}
These are the sampl API methods.
- (void)getDataOneWithCompletion:(void(^)(DataOneModel *response))completion {
[APIManager getDataOneWithResponse:^(DataOneModel *response) {
completion(response)
}];
}
- (void)getDataTwoWithCompletion:(void(^)(DataTwoModel *response))completion {
[APIManager getDataTwoWithResponse:^(DataTwoModel *response) {
completion(response)
}];
}
// And 4 more of these API call methods.
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
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 have started to explore the new XCTest APIs for asynchronous and performance testing. In isolation, the Apple examples from WWMC work well, but I have been unable to figure out how to combine them. The best I have been able to come up with is the following, but I receive the following error when it runs:
API violation - call made to wait without any expectations having been set.
XCTestExpectation *clsQueryReturnedExpectation = [self expectationWithDescription:#"clsQuery returned"];
PFCLSClient *theClient = [[PFCLSClient alloc] init];
[self measureMetrics:#[XCTPerformanceMetric_WallClockTime] automaticallyStartMeasuring:YES forBlock: ^{
[theClient getStructureOfType:clsImageTypeSVG ForID:idString success: ^(NSDictionary *structureInfo) {
[clsQueryReturnedExpectation fulfill];
} failure: ^(NSError *error) {
XCTFail();
[clsQueryReturnedExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler: ^(NSError *error) {
[self stopMeasuring];
}];
}];
Has anyone been able to accomplish something similar?
Thx
With some help from Apple, I have a solution. Silly oversight on my part as this is very easy to solve. To get to work, all you need to do is put the creating of the expectation object (clsQueryReturnedExpectation) inside the measureMetrics block so it is created afresh each time the performance test is run.
PFCLSClient *theClient = [[PFCLSClient alloc] init];
[self measureMetrics:#[XCTPerformanceMetric_WallClockTime] automaticallyStartMeasuring:YES forBlock: ^{
XCTestExpectation *clsQueryReturnedExpectation = [self expectationWithDescription:#"clsQuery returned"];
[theClient getStructureOfType:clsImageTypeSVG ForID:idString success: ^(NSDictionary *structureInfo) {
[clsQueryReturnedExpectation fulfill];
} failure: ^(NSError *error) {
XCTFail();
[clsQueryReturnedExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler: ^(NSError *error) {
[self stopMeasuring];
}];
}];
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