Asynchronous testing in Swift

In this post I will talk about asynchronous testing in Swift.

As we saw in this post and also in this other one, closures are one of the most important building block of Swift. They are extensively used inside the iOS SDK.
But in the previous posts about closures I didn’t answer one very important question: how can you do unit test asynchronous operation and closure? It seems Apple has the answer for us!! Inside the iOS Testing framework we have expectations.

How do they work? To test that asynchronous operations (and closure) behave as expected, you create one or more expectations within your test, and then fulfill those expectations when the asynchronous operation completes successfully. Your test method waits until all expectations are fulfilled or a specified timeout expires. The general code structure for expectation with closure is like the following example:

let expectation = XCTestExpectation(
description: "Expectation description"
)
yourInstance.method(param: "aParam") {
<Your assert using XCTAssert...>
expectation.fulfill()
}
wait(for: [expectation], timeout: <timeout for fulfillment>)

Basically to test asynchronous operation/closure you must:

  • create an expectation that is an instance of XCTestExpectation
  • execute your closure, make your assert on the closure return value/parameter and call the method fulfill of XCTestExpectation

So, what about a more complex example? Let’s see how powerful expectation are and most importantly how we can test them. Suppose for example we have a use case class called PasswordUpdateUseCase with the following implementation:

public class PasswordUpdateUseCase {
private let passwordService: PasswordService
private let passwordRepository: PasswordRepository

public init(
passwordService: PasswordService,
passwordRepository: PasswordRepository
) {
self.passwordService = passwordService
self.passwordRepository = passwordRepository
}

public func update(password: String) {
passwordService.update(password: password) {
success, error in
if success {
self.passwordRepository.save(password: password)
}
}
}
}

As you can see inside the update method we have an instance of PasswordService that, as the method name suggest, execute an update of the user password and return the result of the operation inside a closure. How do we unit test? Let’s see how we can achieve our objective using some handmade mock and expectation. For this post I will NOT USE the “Given-then-when” structure I used in a previous post, because I want to keep the focus on the code structure. First of all, to test our use case we need to mock the PasswordRepository. In our test we want to verify if our save method has been called or not. We can achieve this objective by implementing a spy object, PasswordDatabaseRepositorySpy, that exposes a status property savePasswordHasBeenCalled.

class PasswordDatabaseRepositorySpy: PasswordRepository {
private(set) var savePasswordHasBeenCalled = false

func save(password: String) {
savePasswordHasBeenCalled = true
}
}

Now it’s time to mock our PasswordService. We need to mock it so that it has the following features:

  • it exposes a status property that let us know if the method update has been called
  • it simulates an asynchronous call inside the update method
  • it can fullfil the expecation of our test in time

A lot of stuff to do. Let’s see how we can implement it. We will call it PasswordNetworkServiceSpy.

class PasswordNetworkServiceSpy: PasswordService {
private(set) var updatePasswordHasBeenCalled = false
private let expectation: XCTestExpectation
private let successful: Bool
init(expectation: XCTestExpectation, successful: Bool) {
self.expectation = expectation
self.successful = successful
}

func update(
password: String,
completion: @escaping (Bool, Error) -> ()
) {
DispatchQueue.main.asyncAfter(
deadline: .now() + .milliseconds(200)
) {
self.updatePasswordHasBeenCalled = true
completion(
self.successful,
NSError(domain: "error", code: -1, userInfo: nil)
)
self.expectation.fulfill()
}
}
}

The interesting thing of our implementation is that our spy will be in charge of fulfill the expectation, because the closure executed inside the PasswordUpdateUseCase is created inside our PasswordService spy, and we have to be sure that after its execution the expectation.fulfill() is called.
Now we are ready to write our unit tests. We will test two cases: update successful and update failure.

class AsynchronousTestingClosureDependencyTests: XCTestCase {

func testUseCaseUpdatePasswordSuccessful() {
let updateExpectation = expectation(
description: "updateExpectation"
)
let service = PasswordNetworkServiceSpy(
expectation: updateExpectation,
successful: true
)
let repository = PasswordDatabaseRepositorySpy()
let passwordUseCase = PasswordUpdateUseCase(
passwordService: service,
passwordRepository: repository
)
passwordUseCase.update(password: "::password::")
wait(for: [updateExpectation], timeout: 300)
XCTAssertTrue(service.updatePasswordHasBeenCalled)
XCTAssertTrue(repository.savePasswordHasBeenCalled)
}

func testUseCaseUpdatePasswordFail() {
let updateExpectation = expectation(
description: "updateExpectation"
)
let service = PasswordNetworkServiceSpy(
expectation: updateExpectation,
successful: false
)
let repository = PasswordDatabaseRepositorySpy()
let passwordUseCase = PasswordUpdateUseCase(
passwordService: service,
passwordRepository: repository
)
passwordUseCase.update(password: "::password::")
wait(for: [updateExpectation], timeout: 300)
XCTAssertTrue(service.updatePasswordHasBeenCalled)
XCTAssertFalse(repository.savePasswordHasBeenCalled)
}
}

As you can see in this test we have an example of an expectation creation/usage. In each test we are calling the wait(for: [updateExpectation], timeout: 300) so that the tests will “wait” until the expectation is fulfilled or the max timeout is reach (and in the last case the test fails, no matter the other condition). The most strange thing is related to the order of instruction between the wait and the various XCTAssert. To make our tests work we need to wait until the closure inside the update method of the use case is completed. Then we can make our assertion and verify that our conditions are verified to make our test pass (so, in this case, we can verify that our various method on the various collaborators have/have not been called). We are done with our example. As you can see you can experiment a little bit with expectations and implement complex patterns to verify your closure. You can find the complete example discussed above here. Expectation: your true friend for asynchronous code testing ❤️.

Originally published at www.fabrizioduroni.it on May 31, 2018.

--

--

--

I'm a software developer 🤓. I ❤️ mobile application development, computer graphics and web development. I ❤️ computers. https://www.fabrizioduroni.it/

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Publish Me! Exploring AMP, Medium, and Apple News.

Cultivating the Automation Mindset

Think Agile? Think recycling!

Monki Gras 7

Give me Overlap CIDRs(AWS)

10+ free Tools that make web developers Life Easy

The SOLID Principle Applied to Swift

[Guide] Releasing a Huawei Quick Game Using the LayaAir Engine

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Fabrizio Duroni

Fabrizio Duroni

I'm a software developer 🤓. I ❤️ mobile application development, computer graphics and web development. I ❤️ computers. https://www.fabrizioduroni.it/

More from Medium

Swift Basics In One Article

Swift Concurrency Task Management

Handling dynamic JSON value using Decodable

Fundamental Design Pattern: Delegation