Xcode 6 now handles asynchronous tests withXCTestExpectation
. When testing an asynchronous process, you establish the "expectation" that this process will complete asynchronously, after issuing the asynchronous process, you then wait for the expectation to be satisfied for a fixed amount of time, and when the query finishes, you will asynchronously fulfill the expectation.
For example:
- (void)testDataTask
{
XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];
NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error, @"dataTaskWithURL error %@", error);
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
}
XCTAssert(data, @"data nil");
// do additional tests on the contents of the `data` object here, if you want
// when all done, Fulfill the expectation
[expectation fulfill];
}];
[task resume];
[self waitForExpectationsWithTimeout:10.0 handler:nil];
}
My previous answer, below, predates XCTestExpectation
, but I will keep it for historical purposes.
Because your test is running on the main queue, and because your request is running asynchronously, your test will not capture the events in the completion block. You have to use a semaphore or dispatch group to make the request synchronous.
For example:
- (void)testDataTask
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error, @"dataTaskWithURL error %@", error);
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
}
XCTAssert(data, @"data nil");
// do additional tests on the contents of the `data` object here, if you want
// when all done, signal the semaphore
dispatch_semaphore_signal(semaphore);
}];
[task resume];
long rc = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 60.0 * NSEC_PER_SEC));
XCTAssertEqual(rc, 0, @"network request timed out");
}
The semaphore will ensure that the test won't complete until the request does.
Clearly, my above test is just making a random HTTP request, but hopefully it illustrates the idea. And my various XCTAssert
statements will identify four types of errors:
The NSError
object was not nil.
The HTTP status code was not 200.
The NSData
object was nil.
The completion block didn't complete within 60 seconds.
You would presumably also add tests for the contents of the response (which I didn't do in this simplified example).
Note, the above test works because there is nothing in my completion block that is dispatching anything to the main queue. If you're testing this with an asynchronous operation that requires the main queue (which will happen if you are not careful using AFNetworking or manually dispatch to the main queue yourself), you can get deadlocks with the above pattern (because we're blocking the main thread waiting for the network request to finish). But in the case of NSURLSession
, this pattern works great.
You asked about doing testing from the command line, independent of the simulator. There are a couple of aspects:
If you want to test from the command line, you can use xcodebuild
from the command line. For example, to test on a simulator from the command line, it would be (in my example, my scheme is called NetworkTest
):
xcodebuild test -scheme NetworkTest -destination 'platform=iOS Simulator,name=iPhone Retina (3.5-inch),OS=7.0'
That will build the scheme and run it on the specified destination. Note, there are many reports of Xcode 5.1 issues testing apps on simulator from the command line with xcodebuild
(and I can verify this behavior, because I have one machine on which the above works fine, but it freezes on another). Command line testing against simulator in Xcode 5.1 appears to be not altogether reliable.
If you don't want your test to run on the simulator (and this applies whether doing it from the command line or from Xcode), then you can build a MacOS X target, and have an associated scheme for that build. For example, I added a Mac OS X target to my app and then added a scheme called NetworkTestMacOS
for it.
BTW, if you add a Mac OS X scheme to and existing iOS project, the tests might not be automatically added to the scheme, so you might have to do that manually by editing the scheme, navigate to the tests section, and add your test class there. You can then run those Mac OS X tests from Xcode by choosing the right scheme, or you can do it from the command line, too:
xcodebuild test -scheme NetworkTestMacOS -destination 'platform=OS X,arch=x86_64'
Also note, if you've already built your target, you can run those tests directly by navigating to the right DerivedData
folder (in my example, it's ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug
) and then running xctest
directly from the command line:
/Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest
Another option for isolating your tests from your Xcode session is to do your testing on a separate OS X Server. See the Continuous Integration and Testing section of the WWDC 2013 video Testing in Xcode. To go into that here is well beyond the scope of the original question, so I'll simply refer you to that video which gives a nice introduction to the topic.
Personally, I really like the testing integration in Xcode (it makes the debugging of tests infinitely easier) and by having a Mac OS X target, you bypass the simulator in that process. But if you want to do it from the command line (or OS X Server), maybe the above helps.