75

I'm writing integration tests in Xcode 6 to go alongside my unit and functional tests. XCTest has a setUp() method that gets called before every test. Great!

It also has XCTestException's which let me write async tests. Also great!

However, I would like to populate my test database with test data before every test and setUp just starts executing tests before the async database call is done.

Is there a way to have setUp wait until my database is ready before it runs tests?

Here's an example of what I have do now. Since setUp returns before the database is done populating I have to duplicate a lot of test code every test:

func test_checkSomethingExists() {

    let expectation = expectationWithDescription("")
    var expected:DatabaseItem

    // Fill out a database with data. 
    var data = getData()
    overwriteDatabase(data, {
      // Database populated.
      // Do test... in this pseudocode I just check something...
      db.retrieveDatabaseItem({ expected in

        XCTAssertNotNil(expected)

        expectation.fulfill()
      })
    })

    waitForExpectationsWithTimeout(5.0) { (error) in
        if error != nil {
            XCTFail(error.localizedDescription)
        }
    }

}

Here's what I would like:

class MyTestCase: XCTestCase {

    override func setUp() {
        super.setUp()

        // Fill out a database with data. I can make this call do anything, here
        // it returns a block.
        var data = getData()
        db.overwriteDatabase(data, onDone: () -> () {

           // When database done, do something that causes setUp to end 
           // and start running tests

        })        
    }

    func test_checkSomethingExists() {

        let expectation = expectationWithDescription("")
        var expected:DatabaseItem


          // Do test... in this pseudocode I just check something...
          db.retrieveDatabaseItem({ expected in

            XCTAssertNotNil(expected)

            expectation.fulfill()
        })

        waitForExpectationsWithTimeout(5.0) { (error) in
            if error != nil {
                XCTFail(error.localizedDescription)
            }
        }

    }

}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Brett Elliot
  • 922
  • 1
  • 6
  • 9
  • If you search stack overflow for "[ios] asynchronous unit test" you'll see a ton of answers with not only the `XCTestExpectation` (not `XCTestException`) technique, but also the semaphore technique. e.g. http://stackoverflow.com/a/23658385/1271826. You can probably use the semaphore technique for your async database code (though you haven't shared how you're doing this database stuff so we can't be more specific than that). I'm surprised that your database library doesn't have a synchronous feature, because that's very common in database libraries. – Rob Apr 08 '15 at 02:24
  • Rob, I edited my question to show exactly what I'm looking for. I do know how to use XCTest and XCTestException to write async tests. What I don't know is how to keep the tests from running until setUp is done. Thanks. – Brett Elliot Apr 08 '15 at 12:16
  • Lol. Again, no such thing as `XCTestException`. It's `XCTestExpectation`. And as I said, use semaphore technique in `setUp`, not `XCTestExpectation`. (Use expectations in the tests, but in `setUp` use semaphores.) – Rob Apr 08 '15 at 12:36
  • Re: XCTestException --- code dyslexia strikes again! lol – Brett Elliot Apr 08 '15 at 14:25
  • Is it possible to put your database set up code in a helper method? Then you only have one duplicated line per test. – Joe Masilotti Jan 30 '16 at 14:55

5 Answers5

127

Rather than using semaphores or blocking loops, you can use the same waitForExpectationsWithTimeout:handler: function you use in your async test cases.

// Swift
override func setUp() {
    super.setUp()

    let exp = expectation(description: "\(#function)\(#line)")

    // Issue an async request
    let data = getData()
    db.overwriteDatabase(data) {
        // do some stuff
        exp.fulfill()
    }

    // Wait for the async request to complete
    waitForExpectations(timeout: 40, handler: nil)
}

// Objective-C
- (void)setUp {
    [super setUp];

    NSString *description = [NSString stringWithFormat:@"%s%d", __FUNCTION__, __LINE__];
    XCTestExpectation *exp = [self expectationWithDescription:description];

    // Issue an async request
    NSData *data = [self getData];
    [db overwriteDatabaseData: data block: ^(){
        [exp fulfill];
    }];        

    // Wait for the async request to complete
    [self waitForExpectationsWithTimeout:40 handler: nil];
}
RndmTsk
  • 1,704
  • 2
  • 12
  • 12
  • 4
    Great! This is what Apple recommends - search on XCTestExpectation in the Xcode API reference. Wish I could double up vote for including both ObjC and Swift! – David H Aug 12 '16 at 18:17
  • 7
    This should be the accepted answer. There's a great post from NSHipster explaining how to work with asynchronous testing http://nshipster.com/xctestcase/. – isanjosgon Oct 12 '16 at 10:34
  • 14
    As a side note, this does not work when you are using override class func setup() – Mizmor Apr 25 '17 at 20:37
36

There are two techniques for running asynchronous tests. XCTestExpectation and semaphores. In the case of doing something asynchronous in setUp, you should use the semaphore technique:

override func setUp() {
    super.setUp()

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    let semaphore = DispatchSemaphore(value: 0)

    db.overwriteDatabase(data) {

        // do some stuff

        semaphore.signal()
    }

    semaphore.wait()
}

Note, for that to work, this onDone block cannot run on the main thread (or else you'll deadlock).


If this onDone block runs on the main queue, you can use run loops:

override func setUp() {
    super.setUp()

    var finished = false

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    db.overwriteDatabase(data) {

        // do some stuff

        finished = true
    }

    while !finished {
        RunLoop.current.run(mode: .default, before: Date.distantFuture)
    }
}

This is a very inefficient pattern, but depending upon how overwriteDatabase was implemented, it might be necessary

Note, only use this pattern if you know that onDone block runs on the main thread (otherwise you'll have to do some synchronization of finished variable).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Is there anyway to make this work if the onDone block runs in the main thread? – Brett Elliot Apr 08 '15 at 14:40
  • 1
    I've updated this with example that uses runloops if `onDone` runs on main thread. – Rob Apr 08 '15 at 15:47
  • 1
    Is there a way to do asynchronous one time setup inside override class func setUp() { } that should be called only once for all test cases? – Ilker Baltaci Apr 03 '20 at 13:44
  • @IlkerBaltaci - I would have thought that you’d just do this in the `class` [rendition](https://developer.apple.com/documentation/xctest/xctestcase/1496262-setup). See [Understanding Setup and Teardown for Test Methods](https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods). – Rob Apr 03 '20 at 14:59
5

Swift 4.2

Use this extension:

import XCTest

extension XCTestCase {
    func wait(interval: TimeInterval = 0.1 , completion: @escaping (() -> Void)) {
        let exp = expectation(description: "")
        DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
            completion()
            exp.fulfill()
        }
        waitForExpectations(timeout: interval + 0.1) // add 0.1 for sure `asyncAfter` called
    }
}

and usage like this:

func testShouldDeleteSection() {
        let tableView = TableViewSpy()
        sut.tableView = tableView
        
        sut.sectionDidDelete(at: 0)
        
        wait {
            XCTAssert(tableView.isReloadDataCalled, "Check reload table view after section delete")
        }
    }

The example above isn't complete, but you can get the idea. Hope this helps.

Jason Moore
  • 7,169
  • 1
  • 44
  • 45
moraei
  • 1,443
  • 1
  • 16
  • 15
  • To get this working, you need to dispatch to other than main queue. If you do this on a main queue, completion is never called. This works `DispatchQueue.global().asyncAfter(deadline: .now() + interval)`. Keep in mind though, that this is not ideal because you lose hardcoded 0.1 seconds per test. Which might be too much. – Vilém Kurz Oct 29 '22 at 09:13
1

Swift 5.5 & iOS 13+

You could overridefunc setUp() async throws for instance:

final class MyTestsAsync: XCTestCase {
    var mockService: ServiceProtocolMock!

    override func setUp() async throws {
        mockService = await {
            //... some async setup
        }()
    }

    override func tearDown() async throws {
        //...

See Apple docs on concurrency note here

Radek
  • 581
  • 4
  • 12
0

If you're here because you're trying to solve this problem for your class setUp method, consider using semaphores since XCTestExpectation can only be used in class instance methods. For example:

override class func setUp() {
    let semaphore = DispatchSemaphore(value: 0)
    someAsyncFunction {
        ... do stuff ...
        semaphore.signal()
    }
    semaphore.wait()   
}
sherb
  • 5,845
  • 5
  • 35
  • 45