Swift Tdd & Async Urlsession - How to Test

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:

  1. The NSError object was not nil.

  2. The HTTP status code was not 200.

  3. The NSData object was nil.

  4. 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:

  1. 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.

  2. 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
  3. 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.

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:

  1. Create a XCTestCase subclass by click command+N:

New_file


  1. 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;

    @end
  2. Setup 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);
    }
  3. 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];
    }
  4. 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.


  1. 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];
    }
  2. 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:

Coverage

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



Leave a reply



Submit