ExpectToEventuallyEqual

1.1.0

XCTest assertion for async code
jonreid/ExpectToEventuallyEqual

What's New

Support back to Swift 5.6

2023-12-22T23:13:18Z

22 Dec 2023

  • Lower Swift requirement to 5.6.

ExpectToEventuallyEqual

Build Mastodon Follow

ExpectToEventuallyEqual is an XCTest assertion for asynchronous code.

Contents

Example

Let's say we have a table view that reads from a view model. So the view model determines the number of rows in the table:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    viewModel.result.count
}

snippet source | anchor

The table view controller's viewDidLoad() tells the view model to load, then reloads the table data. Because the load() is asynchronous, we await its results and wrap this inside a Task.

Task {
    await viewModel.load()
    self.tableView.reloadData()
}

snippet source | anchor

If this Task included a call to a closure, tests could wait on an XCTestExpectation and inject a closure which calls fulfill() on the expectation. So one approach to testing this is to add a completion closure that fires after the data reloads.

Another approach is to check for some condition in periodic intervals. After stubbing two results into the view model, here's an expectToEventuallyEqual assertion that checks that the number of rows will eventually be 2:

try expectToEventuallyEqual(
    actual: { tableDataSource.tableView(sut.tableView, numberOfRowsInSection: 0) },
    expected: 2
)

snippet source | anchor

The assertion repeatedly evaluates the actual closure, comparing it to the expected value. As soon as they are equal, this assertion will pass. If it times out with the values remaining unequal, the assertion fails.

Example failure says test_numberOfRows(): failed - Expected 2, but was 1 after 93 tries, timing out after 1.0 seconds

There is a SampleApp you can try to see some passing tests and one failing test.

How To Install

Swift Package Manager

If you have a Project.swift file, declare the following dependency:

dependencies: [
    .package(url: "https://github.com/jonreid/ExpectToEventuallyEqual", from: "1.0.0"),
],

Then add it to your test target:

.testTarget(
    name: "MyTests",
    dependencies: [
        "ExpectToEventuallyEqual",
    ],

Details

The actual closure is allowed to throw exceptions.

The actual closure and and expected value must evaluate to the same Equatable type.

The default timeout is 1 second. You can specify a different value.

After every comparison, the RunLoop runs briefly to process other events on the thread.

On failure, the assertion reports the expected value and the last actual value. If they are strings, the values are shown in double quotes with the following characters represented as escaped special characters:

  • \" (double quotation mark)
  • \n (line feed)
  • \r (carriage return)
  • \t (horizontal tab)

Origin

Jon Reid and Steven Baker created this assertion while teaching an iOS workshop together.

Description

  • Swift Tools 5.6.0
View More Packages from this Author

Dependencies

  • None
Last updated: Thu Apr 04 2024 17:56:56 GMT-0900 (Hawaii-Aleutian Daylight Time)