Testing Asynchronous Code

Episode #333 | 4 minutes | published on March 29, 2018 | Uses swift-4, Xcode-9.2
Subscribers Only
In this episode we cover the concept of expectations, which enables us to test asynchronous code, properly timing out and failing a test if the expectation is never fulfilled.

Testing Asynchronous Code

Let’s assume we have some code we want to test but the work is done asynchronously and we are called back when the work is completed.
For the sake of this example, we can create a dummy class that simulates this type of behavior.

import XCTest

class Job {
    var finished = false

    func run(completion: @escaping ()->Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.finished = true
            completion()
        }
    }
}

Testing Async Code Directly

If we run write a test that runs the code above and asserts that it completed, then the test will fail…

Using Expectations

To get around this we want our tests to wait a bit before running the assertions. We also need a way to time out and fail if the code never completes. We can use the expectation method on XCTestCase which gives us a handle that we can wait on

class TestAsyncCode : XCTestCase {
    func testJobFinishes() {
        let exp = expectation(description: "Completion block called")
        // ...
    }
}

Then we can fulfill this expectation when the callback is completed, signaling to the framework that we don’t need to wait any longer

class TestAsyncCode : XCTestCase {
    func testJobFinishes() {
        // ...
        job.run {
            exp.fulfill()
            // ...
        }
    }
}

Finally we can tell the test to wait for up to 3 seconds for this expectation to be met:

class TestAsyncCode : XCTestCase {
    func testJobFinishes() {
        let exp = expectation(description: "Completion block called")
        let job = Job()
        job.run {
            exp.fulfill()
            XCTAssertTrue(job.finished)
        }
        waitForExpectations(timeout: 3, handler: nil)
    }
}

So I'm going to talk about

testing asynchronous code.

And so I have a dummy

class here called Job,

and it has a finished property,

and we want to simulate

some work that is going to be done,

and then it'll set finished

to true when it's finished.

So, we're going to use

DispatchQueue.main.asyncAfter,

and we're going to execute

this after, say, two seconds.

And then we're going to

set finished to true.

So this will simulate that

some work is being done,

and then eventually we

call ourselves back.

So here, I want to create

a func testJobFinishes,

and we're going to create a job,

and then we're going

to tell the job to run,

and I want to XCTAssertEqual

or AssertTrue basically

that the job is finished.

And if we run this test,

notice that this failed.

It failed immediately,

because we didn't actually

give it enough time.

It just dropped straight through,

and it never gave enough

time for this job to finish.

And this is going to be common

with all of our async code.

Our test is going to run from

top to bottom immediately.

In our job.run method,

we're kicking off some task

on a background thread, or

rather, on a main thread.

This is going to wait for the

scheduler to actually queue

this up two seconds later

to say that it's finished.

And so we actually need to

wait for this test to finish

in order, before we run

our expectations here.

So, what we can do here

is, and oftentimes,

we'll have like completion

blocks and things like that,

so I'm going to add a

completion block here, which is

a function that takes

nothing, returns nothing,

and we'll call the completion

block when it's done,

which means that that

needs to be escaping.

And then here, we can pass

in this completion block.

So now we have this problem where it looks

like our test actually passed.

It dropped straight through and passed.

And that's problematic, because we never

actually got to run this assertion, right.

This completion block never ran.

And so this is the problem that we have.

What we need to do is wait

until this completion block

actually runs and then run our assertion.

So what we're going to do

here is we're going to create

an expectation, and there's

an expectation method

on XCTestCase, which

gives us a description.

So we can say completion block called.

And then later on down here,

we want to call waitForExpectations.

You can wait for all

expectations or specific ones.

In this case, I'm going to wait

for any expectations that I've created,

and I'm going to wait up to three seconds.

And when that fails, I'm

going to have this error,

and I can say something like it failed.

Another option is just to pass nil here.

If you don't really care, you

just want the test to fail,

then you can pass nil in for the handler.

So, in this case, now

we can see that our test

is actually taking a long longer to run.

It started, and then it failed

because the asynchronous wait failed.

It exceeded timeout of three seconds

with unfulfilled expectations.

So we need to tell it

when that expectation is

actually fulfilled, which

happens here in this block.

We're going to call it exp.fulfill,

which tells our

expectation that it's done.

It no longer needs to wait for that.

And here, you can see that now,

we're actually finishing our job.

It does actually run this

expectation, XCTAssertTrue,

and things are working nicely.

Now, obviously, in a playground,

this kind of is less than desirable,

so I'm going to change these

parameters to a little bit

smaller values so we

can see our test passing

a little bit faster.

But this is how we test asynchronous code.

If we wanted to use this

TestExpectation stuff

outside of XCTestCase,

perhaps in a test library

or helper functions or things like that,

then we can expect the XCTWaiter class,

and this will allow us

to do the same thing,

but outside of the test

case instance itself.

And that's something

that we may investigate

in a future screencast.

So, this is the basic structure

for how to test asynchronous

code using XCTest.

blog comments powered by Disqus