How to test a method that contains Task Async/await in swift
Ideally, as you imply, your interactor
would be declared using a protocol so that you can substitute a mock for test purposes. You then consult the mock object to confirm that the desired method was called. In this way you properly confine the scope of the system under test to answer only the question "was this method called?"
As for the structure of the test method itself, yes, this is still asynchronous code and, as such, requires asynchronous testing. So using an expectation and waiting for it is correct. The fact that your app uses async/await to express asynchronousness does not magically change that! (You can decrease the verbosity of this by writing a utility method that creates a BOOL predicate expectation and waits for it.)
Setting up delegate for NSURLSession and unit testing it
Alright I figured it out myself with the help of Apple's documentation for NSURLSession and the blog at http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/. I've tested it with the live URL for now, I'll update the answer when I add API mocking to my unit test and make the unit test more robust.
The short of it is that NSURLSession
has its own delegate methods, and in this case where dataTaskWithRequest
is used, an NSURLSessionDataDelegate
can be set up and used to retrieve the API results.
The code for the delegate declaration is mostly correct, I just needed to change NSURLSessionDelegate
to NSURLSessionDataDelegate
in the header file.
The unit test requires a bit of set up, but is otherwise pretty straightforward. It involves initialising the class with the NSURLSession
call, setting the object's delegate to self
, and initialise a flag variable to NO
. The flag variable will be set to YES
when the delegate is called, which is what I am initially testing for. The fully set up unit test is below.
@interface APIDelegateTests : XCTestCase <NSURLSessionDataDelegate>
{
PtvApi *testApi;
BOOL callbackInvoked;
}
@end
@implementation APIDelegateTests
- (void)setUp {
[super setUp];
testApi = [[PtvApi alloc] init];
testApi.delegate = self;
callbackInvoked = NO;
}
- (void)tearDown {
testApi.delegate = nil;
[super tearDown];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
callbackInvoked = YES;
}
// Method is credit to Claus Brooch.
// Retrieved from http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/ on 10/04/2016
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
} while (!callbackInvoked);
return callbackInvoked;
}
- (void)testThatApiCallbackWorks {
[testApi ptvApiHealthCheck];
XCTAssert([self waitForCompletion:30.0], @"Testing to see what happens here...");
}
@end
Test Driven Upload method?
The approach to take is to use a a test double to check that the correct networking calls are made by your upload
method. You'll be making an asynchronous call to a networking library, which may be URLSession
or may be another library such as AlamoFire. It shouldn't matter to your upload
method which library is in use.
To achieve this, you want to avoid directly using URLSession, and use a wrapper which conforms to an interface that you can then mock in your tests. This means that your code will use a different implementation of the networking class at runtime than at test time, and you'll "inject" the correct one as required.
For example, you could have this interface to your networking library:
protocol NetworkRequesting {
func post(data: Data, url: URL)
}
With the following real implementation to be used at runtime:
struct NetworkRequester: NetworkRequesting {
func post(data: Data, url: URL) {
let session = URLSession()
let task = session.uploadTask(with: URLRequest(url: url), from: data)
task.resume()
}
}
However, at test time, you use the following mock instead:
class MockNetworkRequester: NetworkRequesting {
var didCallPost = false
var spyPostData: Data? = nil
var spyPostUrl: URL? = nil
func post(data: Data, url: URL) {
didCallPost = true
spyPostData = data
spyPostUrl = url
}
}
And then, given the following class under test:
class ImageUploader {
let networkRequester: NetworkRequesting
init(networkRequester: NetworkRequesting) {
self.networkRequester = networkRequester
}
func upload(image: UIImage, url: URL) {
}
}
You can test the implementation of upload
like so:
class UploadImageTests: XCTestCase {
func test_uploadCallsPost() {
let mockNetworkRequester = MockNetworkRequester()
let uploader = ImageUploader(networkRequester: mockNetworkRequester)
uploader.upload(image: UIImage(), url: URL(string:"http://example.com")!)
XCTAssert(mockNetworkRequester.didCallPost)
}
}
Currently, that test will fail as upload
does nothing, but if you put the following into the class under test, the test will pass:
func upload(image: UIImage, url: URL) {
guard let otherUrl = URL(string:"https://example.org") else { return }
networkRequester.post(data: Data(), url: otherUrl)
}
And that's your first TDD cycle. Clearly it's not yet behaving as you'd like, so you need to write another test to make sure that the url used is the one you expect, or the data passed is what you expect.
There are a number of ways to get your code to use the real network requester at runtime, you could have the init method use default parameter values to get it to use NetworkRequester
, or use a static factory method to create it, and there are other options like Inversion of Control, which is well beyond the scope of this answer.
The important thing to remember is that you're testing that you make the correct calls to the networking framework, you're not testing the networking framework. I like to keep my protocol interfaces pretty declarative, passing the things required to make a request in any framework, but you might find you prefer to go closer to the metal and essentially mirror the implementation of URLSession - it's up to you, and more of an art than a science, in my opinion.
How to write unit test for `downloadTask` with `completionHandler `?
The standard idiom is to use Xcode asynchronous testing APIs, mostly the XCTestExpectation
class. Those are available since Xcode 6 so, if you are using a recent version, you should be covered ;)
The general async test template is as follows:
func testAsyncFunction() {
let asyncDone = expectation(description: "Async function")
...
someAsyncFunction(...) {
...
asyncDone.fulfill()
}
wait(for: [asyncDone], timeout: 10)
/* Test the results here */
}
This will block your test execution until the aforementioned function completes (or the specified timeout has elapsed).
How do I unit test HTTP request and response using NSURLSession in iOS 7.1?
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 calledNetworkTest
):
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 runningxctest
directly from the command line:
/Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctestAnother 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.
How to write a unit test for a method with a completion handler that returns data?
You should use OCMArg with checkWithBlock or invokeBlockWithArgs to test completions handlers. Here an example:
- Create a XCTestCase subclass by click command+N:
Add properties for testable instance and necessary mocks:
@interface SomeClassTests : XCTestCase
@property (nonatomic, strong) SomeClass *testableInstance;
@property (nonatomic, strong) NSURLSession *mockSession;
@property (nonatomic, strong) NSURLRequest *mockRequest;
@property (nonatomic, strong) NSHTTPURLResponse *mockResponse;
@endSetup properties:
- (void)setUp
{
[super setUp];
self.testableInstance = [SomeClass new];
self.mockSession = OCMClassMock([NSURLSession class]);
self.mockRequest = OCMClassMock([NSURLRequest class]);
self.mockResponse = OCMClassMock([NSHTTPURLResponse class]);
OCMStub(ClassMethod([(id)self.mockSession sharedSession])).andReturn(self.mockSession);
}Don't forget to clean up at tear down:
- (void)tearDown
{
[(id)self.mockSession stopMocking];
self.mockResponse = nil;
self.mockRequest = nil;
self.mockSession = nil;
self.testableInstance = nil;
[super tearDown];
}Let's test the case when an error is occurs:
- (void)testWhenErrorOccuersThenCompletionWithSameError
{
// arrange
NSError *givenError = [[NSError alloc] initWithDomain:@"Domain" code:0 userInfo:nil];
OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg invokeBlockWithArgs:@"", self.mockResponse, givenError, nil])]);
void (^givenCompletion)(id _Nonnull, NSError * _Nonnull) = ^void(id _Nonnull responseData, NSError * _Nonnull resultError) {
// assert
XCTAssertNil(responseData);
XCTAssertEqual(resultError, givenError);
};
// act
[self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
}
So we will sure that if some error occurs then the completion handler will invokes with same error at the argument.
Let's test when we get some bad status code:
- (void)testWhenBadStatusCodeThenReturnWithoutCompletion
{
// arrange
OCMStub([self.mockResponse statusCode]).andReturn(403);
OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg checkWithBlock:^BOOL(id param) {
void (^passedCompletion)(NSData *data, NSURLResponse *response, NSError *error) = param;
passedCompletion(nil, self.mockResponse, nil);
return YES;
}])]);
void (^givenCompletion)(id _Nonnull, NSError * _Nonnull) = ^void(id _Nonnull responseData, NSError * _Nonnull resultError) {
// assert
XCTFail("Shouldn't be reached");
};
// act
[self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
}And finally lets test when we actually get data:
- (void)testWhenSuccesThenCompletionWithSameData
{
// arrange
NSData *givenData = [NSData data];
OCMStub([self.mockResponse statusCode]).andReturn(200);
OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg checkWithBlock:^BOOL(id param) {
void (^passedCompletion)(NSData *data, NSURLResponse *response, NSError *error) = param;
passedCompletion(givenData, self.mockResponse, nil);
return YES;
}])]);
void (^givenCompletion)(id _Nonnull, NSError * _Nonnull) = ^void(id _Nonnull responseData, NSError * _Nonnull resultError) {
// assert
XCTAssertEqual(responseData, givenData);
};
// act
[self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
}
If you will switch on a coverage then you will see that such test fully cover testable code:
Errors setting up XCTest using Swift Result type
To create a UserResponse
in test code, call its synthesized initializer, not the decoder initializer. Something like:
let userResponse = UserResponse(user: User("Chris"), token: "TOKEN")
And to create a closure in test code, you need to give it code. Completion closures in tests have one job, to capture how they were called. Something like:
var capturedResponses: [Result<UserResponse, Error>] = []
sut.createAccount(user: UserSignup, completion: { capturedResponses.append($0) })
This captures the Result
that createAccount(user:completion:)
sends to the completion handler.
…That said, it looks like your view model is directly calling a static function that makes a service call. If the test runs, will it create an actual account somewhere? Or do you have some boundary in place that we can't see?
Instead of directly testing createAccount(user:completion:)
, what you probably want to test is:
- That a certain action (signing up) will attempt to create an account for a given user—but not actually do so.
- Upon success, the view model will do one thing.
- Upon failure, the view model will do another thing.
If my assumptions are correct, I can show you how to do this.
Related Topics
Why Can't I Use .Reduce() in a One-Liner Swift Closure with a Variadic, Anonymous Argument
Adding Animation to Tabviews in Swiftui When Switching Between Tabs
How to Capture Multiple Arguments Weakly in a Swift Closure
Command Failed Due to Signal: Segmentation Fault: 11 While Emitting Ir Sil Function
Swift: Differencebetween a Typealias and an Associatedtype with a Value in a Protocol
How to Change Text Color of Actionsheet in Swiftui
Avoid Automatic Framework Linking in Swift
Swift Unit Testing with Xctassertthrows Analogue
Nssortdescriptor Sorting Using Nsdate in Swift
API Violation - Multiple Calls Made to -[Xctestexpectation Fulfill]
How to Select All Text in a Uitextfield Using Swift
Xcode 7 Run on MAC Osx 10.10 Yosemite
Adding Index List and Section Headers to Translated Tableview