Search code examples
swiftxcodeunit-testingasynchronouscloudkit

Is there a conventional way to write a unit test that waits for a CloudKit query to complete?


What is the conventional way to unit test asynchronous operations?

Background:

When my app is initialised it makes a call to CloudKit and updates its internal data source with the results:

import Foundation
import CloudKit

class CountdownItemModel {

    // MARK: - Properties
    var countdownItems: [CountdownItem] = []
    static let sharedInstance = CountdownItemModel()
    let container: CKContainer
    let database: CKDatabase

    // MARK: - Initializers
    init() {
        container = CKContainer.default()
        database = container.privateCloudDatabase
        getAllCountdownItems()
    }

    // MARK - CloudKit Methods
    func getAllCountdownItems() {
        let query = CKQuery(recordType: "CountdownItem", predicate: NSPredicate(value: true))
        database.perform(query, inZoneWith: nil) {
            (results, error) in
            if (error != nil) {
                print("[ERROR] Error in getAllCountdownItems\n", error!)
            }
            else {
                self.countdownItems.removeAll(keepingCapacity: true)
                if let records = results {
                    for record in records {
                        let countdownItem = CountdownItem(record: record)
                        self.countdownItems.append(countdownItem)
                    }
                }
            }
        }
    }
}

Using X-Code's unit testing functionality I've written a test to check that the data from iCloud has arrived, but this test runs before the query has returned, and therefore fails.

import XCTest
@testable import countdown

class countdownTests: XCTestCase {

    let model = CountdownItemModel.sharedInstance

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testItemsCountGreatThanZero() {
        sleep(10) // <- DIRTY FIX/WORKAROUND
        XCTAssert(model.countdownItems.count > 0)
    }
}

As you can see, I've worked around this by inserting a sleep into the test, but this strikes be as a bit messy, and veers into the realm of performance testing rather than unit testing.

Is there a generally accepted approach to this?


Solution

  • When doing async test, you can work with expectation. You can use code like this:

    func testSomething() {
        let exp = expectation(description: "anAsyncCall")
    
        DoSomethingAsync() {
            exp.fulfill()
        }
    
        waitForExpectations(timeout: 10) { error in
            XCTAssertNil(error, "\(error)")
        }
    }
    

    In this case if the async call is finished within 10 seconds then the test will succeed.