Waldo sessions now support scripting! – Learn more
App Development

Waiting in XCTest: A Detailed Guide

Juan Reyes
Juan Reyes
Waiting in XCTest: A Detailed Guide
February 21, 2023
7
min read

XCTest is a testing framework developed by Apple that allows developers to test their iOS and macOS applications. It supplies tools to help write and run automated tests, ensuring the quality of the code before developers release it to the public. In addition, XCTest provides a simple and intuitive way for developers to write unit tests for their code, helping to catch bugs early on and avoid them becoming more significant problems down the road. 

In automated tests, developers often have to wait for certain conditions to be met before making an assertion or continuing with the test. This is where the concept of "waits" comes into play. 

Waits are a mechanism that allows you to pause a test's execution until a specific condition is met. This mechanism can be helpful in many situations, such as when you need to wait for a UI element to become visible before interacting with it or when you need to wait for a network request to complete before checking its results. 

In this post, we'll explain different ways to implement waits in XCTest tests with a step-by-step tutorial and some basic use case examples. We'll cover failure and timing out parameters, multiple expectations, expecting something not to happen, and notifications. 

If you don't have any experience working with XCode, Swift, or the XCTest framework, we recommend you check out this article as a primer on XCTestingthese articles before diving into this one. 

Using the XCTestExpectation Class

The first method for implementing waits in XCTest tests is using the XCTestExpectation class. As you might have guessed, an XCTestExpectation is a condition that you expect to be fulfilled on your code at some point during the test's execution. The test condition will perform a wait until all expectations have been fulfilled before continuing.

Here's an example of how to use an XCTestExpectation to wait for a network request to complete:


// Create expectation let expectation = XCTestExpectation(description: "Wait for network request") // Make the network request makeNetworkRequest { result in // Fulfill the expectation expectation.fulfill() } // Wait for the expectation to be fulfilled wait(for: [expectation], timeout: 10) // Continue with the rest of the test XCTAssertEqual(result, expectedResult)

In this example, we create an expectation with a description of "Wait for a network request." We then make a network request and fulfill the expectation when the request has been completed. Finally, we use the wait(for:timeout:) method to wait for the expectation to be fulfilled. The test will pause its execution until the expectation has been fulfilled or until the timeout of 10 seconds has passed.

Using the XCTWaiter Class

The second method for implementing waits in XCTest tests is using the XCTWaiter class. The XCTWaiter class provides a more flexible and customizable way to wait for specific conditions to be met than the XCTestExpectation condition.

Here's an example of how to use the XCTWaiter class to wait for a UI element to appear:


// Create waiter
let waiter = XCTWaiter()
// Execute waiter
let result = waiter.wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == 1"), object: element)], timeout: 10)
// Evaluate result
switch result {
case .completed:
    // The element exists, continue with the rest of the test
    XCTAssertTrue(element.isHittable)
case .timedOut:
    // The element did not appear within the timeout, the test fails
    XCTFail("Element did not appear within the timeout")
default:
    break
}

<pre class=" language-swift">

<code class=" language-swift">

// Create waiter

let waiter = XCTWaiter()

// Execute waiter

let result = waiter.wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == 1"), object: element)], timeout: 10)

// Evaluate result

switch result {

case .completed:

   // The element exists, continue with the rest of the test

   XCTAssertTrue(element.isHittable)

case .timedOut:

   // The element did not appear within the timeout, the test fails

   XCTFail("Element did not appear within the timeout")

default:

   break

}

</code></pre>

In this example, we create an instance of XCTWaiter and use the wait(for:timeout:) method to wait for the element to exist. The method takes an array of XCTExpectations as its first parameter, and we use the XCTNSPredicateExpectation class to wait for the element to exist. Then, the XCTWaiter will wait for the expectation to be fulfilled or until the timeout of 10 seconds has passed.

Once the XCTWaiter has completed, we check the result to determine what happened. If the result is .completed, it means that the element exists, and we can continue with the rest of the test. On the other hand, if the result is .timedOut, it means the element did not appear within the timeout, and the test will fail.

Failure and Timing Out Parameters

In the examples above, we've seen how to handle failure and timing out when using XCTestExpectation and XCTWaiter. If the expectation is not fulfilled within the specified timeout, the test will fail in both cases.

It's essential to set an appropriate timeout value. Choose one that's sufficient for the condition to be met but not so long that the test takes an unreasonable amount of time to run. The timeout value should be set based on the expected performance of the system and the operation being performed.

Multiple Expectations

Sometimes, you may need to wait for multiple expectations before continuing with the test. For example, you may need to wait for multiple network requests to complete or for various UI elements to appear.

When using XCTestExpectation, you can create multiple expectations and wait for all of them to be fulfilled:


// Create expectations
let expectation1 = XCTestExpectation(description: "Wait for network request 1")
let expectation2 = XCTestExpectation(description: "Wait for network request 2")
// Make the first network request
makeNetworkRequest1 { result in
    // Fulfill the first expectation
    expectation1.fulfill()
}
// Make the second network request
makeNetworkRequest2 { result in
    // Fulfill the second expectation
    expectation2.fulfill()
}
// Wait for both expectations to be fulfilled
wait(for: [expectation1, expectation2], timeout: 10)
// Continue with the rest of the test
XCTAssertEqual(result1, expectedResult1)
XCTAssertEqual(result2, expectedResult2)

When using XCTWaiter, you can use multiple XCTExpectations in the same way:


// Create waiter
let waiter = XCTWaiter()
// Execute waiter
let result = waiter.wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == 1"), object: element1), XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == 1"), object: element2)], timeout: 10)
// Evaluate result
switch result {
case .completed:
    // Both elements exist, continue with the rest of the test
    XCTAssertTrue(element1.isHittable)
    XCTAssertTrue(element2.isHittable)
case .timedOut:
    // One of the elements did not appear within the timeout, the test fails
    XCTFail("Elements did not appear within the timeout")
default:
    break
}

In this example, we use the wait(for:timeout:) method to wait for two elements to exist. The method takes an array of XCTExpectations, and we use the XCTNSPredicateExpectation class to wait for the elements to exist. The XCTWaiter will wait for both expectations to be fulfilled or until the timeout of 10 seconds has passed.

Once the XCTWaiter has completed, we check the result to determine what happened. If the result is .completed, it means that both elements exist, and we can continue with the rest of the test. On the other hand, if the result is .timedOut, it means one of the elements did not appear within the timeout, and the test will fail.

Expecting Something Not to Happen

In addition to waiting for something to happen, you can also use XCTestExpectation and XCTWaiter to wait for something not to happen. For example, you may want to wait for an error message to disappear from the screen—in other words, wait for there not to be a message.

To do this, you can create an expectation for the error message to disappear and use the XCTWaiter to wait for the expectation to be fulfilled:


// Create expectation
let expectation = XCTestExpectation(description: "Wait for error message to disappear")
// Wait for the error message to disappear
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    errorMessage.isHidden = true
    expectation.fulfill()
}
// Waif for the expectation to be fulfilled
wait(for: [expectation], timeout: 10)
// Continue with the rest of the test
XCTAssertTrue(errorMessage.isHidden)

In this example, we create an expectation for the error message to disappear. We then use DispatchQueue.main.asyncAfter to fulfill the expectation after five seconds. The XCTWaiter will then wait for the expectation to be fulfilled or until the timeout of 10 seconds has passed.

Notifications

In some cases, you may need to wait for a notification to be posted before continuing with the test. For example, you may need to wait for a network request to complete and post a notification with the result.

To do this, you can create an expectation for the notification to be posted and use the XCTWaiter to wait for the expectation to be fulfilled:


// Create expectation
let expectation = XCTestExpectation(description: "Wait for network request to complete")
// Observe the notification
let notificationObserver = NotificationCenter.default.addObserver(forName: .networkRequestDidComplete, object: nil, queue: nil) { _ in
    expectation.fulfill()
}
// Perform the network request
networkManager.fetchData()
// Wait for the notification to be posted
wait(for: [expectation], timeout: 10)
// Remove the observer
NotificationCenter.default.removeObserver(notificationObserver)
// Continue with the rest of the test
XCTAssertTrue(networkManager.data != nil)

In this example, we create an expectation for the .networkRequestDidComplete notification to be posted. We then observe the notification using the addObserver() method of the NotificationCenter class. When the notification is posted, the expectation is fulfilled.

Next, we perform the network request and wait for the notification to be posted using the wait(for:timeout:) method. Finally, we remove the observer using the removeObserver() method and continue with the rest of the test. These will confirm that the network request was successful and that data was returned.

Conclusion

In this post, we learned about waiting in XCTest. We also learned how to use XCTestExpectation and XCTWaiter to wait for elements to appear, for something not to happen, and for notifications to be posted. With this knowledge, you can create more robust tests that wait for asynchronous events to complete. This will empower you to better handle test failures and timeouts.

Sometimes, developing and maintaining testing workflows is a time-intensive task. This is especially true for complex applications with many screens and convoluted user interfaces.

However, it doesn't have to be this way. This time and effort could be used to create new and innovative solutions instead.

You're not alone if you find creating a robust testing workflow challenging and expensive. For those looking to reduce this developing cost, consider checking out the Waldo.com UI testing kit. It's effortless to use and doesn't require any coding, even for non-developers.
Start a free trial.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.