Delay/Wait in a test case of Xcode UI testing
Asked Answered
S

14

227

I am trying to write a test case using the new UI Testing available in Xcode 7 beta 2. The App has a login screen where it makes a call to the server to login. There is a delay associated with this as it is an asynchronous operation.

Is there a way to cause a delay or wait mechanism in the XCTestCase before proceeding to further steps?

There is no proper documentation available and I went through the Header files of the classes. Was not able to find anything related to this.

Any ideas/suggestions?

Segalman answered 2/7, 2015 at 10:55 Comment(5)
I think NSThread.sleepForTimeInterval(1) should workAnticline
Great! This looks like it works. But I'm not sure if it's the recommended way to do it. I think Apple should give a better way to do it. Might have to file a RadarSegalman
I actually really think that's okay, it's really the most common way to pause the current thread for a certain time. If you want more control you can also get into GCD (The dispatch_after, dispatch_queue stuff)Anticline
@Anticline Don't tick the run loop - Apple introduced native asynchronous testing in Beta 4. See my answer for details.Gur
Swift 4.0 --> Thread.sleep(forTimeInterval: 2)Tabriz
G
193

Asynchronous UI Testing was introduced in Xcode 7 Beta 4. To wait for a label with the text "Hello, world!" to appear you can do the following:

let app = XCUIApplication()
app.launch()

let label = app.staticTexts["Hello, world!"]
let exists = NSPredicate(format: "exists == 1")

expectationForPredicate(exists, evaluatedWithObject: label, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)

More details about UI Testing can be found on my blog.

Gur answered 26/8, 2015 at 13:32 Comment(7)
Unfortunately there's no way to accept that the timeout happened and move on - waitForExpectationsWithTimeout will automatically fail your test which is quite unfortunate.Suppository
@Suppository Actually, this does not happen for me with XCode 7.0.1.Lalitta
@Lalitta Hmm interesting; I will have to recheck this.Suppository
it doesn't work for me. Here is my sample: let xButton = app.toolbars.buttons["X"] let exists = NSPredicate(format: "exists == 1") expectationForPredicate(exists, evaluatedWithObject: xButton, handler: nil) waitForExpectationsWithTimeout(10, handler: nil)Enidenigma
The app.launch() seems to just relaunch the app. Is it necessary?Djokjakarta
i dont think its necessary Chris, worked for me without itFlaky
this approach is simply to complicated, lord, wish i haven't read itLundquist
T
255

Additionally, you can just sleep:

sleep(10)

Since the UITests run in another process, this works. I don’t know how advisable it is, but it works.

Tetratomic answered 11/12, 2015 at 19:29 Comment(11)
Some time we need the way to delay and don't wanna it raise a failure! thanksSheri
I like NSThread.sleepForTimeInterval(0.2) as you can specify sub-second delays. (sleep() takes an integer parameter; only multiples of a second are possible).Epizootic
@GrahamPerks, yes, though there is also: usleepTetratomic
I wish there was a better answer, but this seems to be the only way right now if you don't want to cause a failure.Skirmish
Even using Joe Masilotti's solution, I still had some weird failures - I suspect XCode/UITest bugs - when trying to test webViews. Adding some sleep() calls fixed that.Bootblack
This comes especially useful in UI TestingPickled
This is really a poor suggestion. If you have a couple of those, your integration tests will last for ever.Sinew
This is appropriate in circumstances where you really need to wait a short period, but its incredibly inconvenient to fulfill an expectation (or would otherwise involve modifying the source in an ugly way)Bergsonism
I think this is really bad suggestion, just increase wait for expectation timeout, never sleep the thread....Rankle
It's not a poor suggestion (you don’t get how UITesting works), but even if it is was a poor suggestion sometimes there is no way to craft an expectation that works (system alerts anyone?) so this is all you have.Tetratomic
I do not understand why this approach is voted so high here since it extend the test time artificially and is really no option when your test suite grows.It prevents running your test suite often - and this is what we all should do! I still go with my approach below (https://mcmap.net/q/117746/-delay-wait-in-a-test-case-of-xcode-ui-testing) which works like a charm.Bechtel
G
193

Asynchronous UI Testing was introduced in Xcode 7 Beta 4. To wait for a label with the text "Hello, world!" to appear you can do the following:

let app = XCUIApplication()
app.launch()

let label = app.staticTexts["Hello, world!"]
let exists = NSPredicate(format: "exists == 1")

expectationForPredicate(exists, evaluatedWithObject: label, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)

More details about UI Testing can be found on my blog.

Gur answered 26/8, 2015 at 13:32 Comment(7)
Unfortunately there's no way to accept that the timeout happened and move on - waitForExpectationsWithTimeout will automatically fail your test which is quite unfortunate.Suppository
@Suppository Actually, this does not happen for me with XCode 7.0.1.Lalitta
@Lalitta Hmm interesting; I will have to recheck this.Suppository
it doesn't work for me. Here is my sample: let xButton = app.toolbars.buttons["X"] let exists = NSPredicate(format: "exists == 1") expectationForPredicate(exists, evaluatedWithObject: xButton, handler: nil) waitForExpectationsWithTimeout(10, handler: nil)Enidenigma
The app.launch() seems to just relaunch the app. Is it necessary?Djokjakarta
i dont think its necessary Chris, worked for me without itFlaky
this approach is simply to complicated, lord, wish i haven't read itLundquist
B
104

iOS 11 / Xcode 9

<#yourElement#>.waitForExistence(timeout: 5)

This is a great replacement for all the custom implementations on this site!

Be sure to have a look at my answer here: https://mcmap.net/q/120094/-asynchronous-ui-testing-in-xcode-with-swift. There I describe an alternative to waiting for requests which will greatly reduce the time your tests are running!

Bechtel answered 31/8, 2017 at 18:32 Comment(5)
Thanks @daidai I changed the text :)Bechtel
Yep this is still the approach I am going for when using XCTestCase and it works like a charm. I do not understand why approaches like sleep(3) are voted so high here since it extends the test time artificially and is really no option when your test suite grows.Bechtel
Actually it requires Xcode 9, but works on devices/simulators running iOS 10 as well ;-)Segregate
Yeah I wrote that on the headline above. But now most people should have upgraded to at least Xcode 9 ;-)Bechtel
My answer is voted that high because I wrote it NINE years ago at the time it was the only option. And it still is the only option in some cases. You don’t always have an element to wait for the existence of. I gave you an upvote, I hope this cools your temper a little.Tetratomic
R
79

Xcode 9 introduced new tricks with XCTWaiter

Test case waits explicitly

wait(for: [documentExpectation], timeout: 10)

Waiter instance delegates to test

XCTWaiter(delegate: self).wait(for: [documentExpectation], timeout: 10)

Waiter class returns result

let result = XCTWaiter.wait(for: [documentExpectation], timeout: 10)
switch(result) {
case .completed:
    //all expectations were fulfilled before timeout!
case .timedOut:
    //timed out before all of its expectations were fulfilled
case .incorrectOrder:
    //expectations were not fulfilled in the required order
case .invertedFulfillment:
    //an inverted expectation was fulfilled
case .interrupted:
    //waiter was interrupted before completed or timedOut
}

sample usage

Before Xcode 9

Objective C

- (void)waitForElementToAppear:(XCUIElement *)element withTimeout:(NSTimeInterval)timeout
{
    NSUInteger line = __LINE__;
    NSString *file = [NSString stringWithUTF8String:__FILE__];
    NSPredicate *existsPredicate = [NSPredicate predicateWithFormat:@"exists == true"];

    [self expectationForPredicate:existsPredicate evaluatedWithObject:element handler:nil];

    [self waitForExpectationsWithTimeout:timeout handler:^(NSError * _Nullable error) {
        if (error != nil) {
            NSString *message = [NSString stringWithFormat:@"Failed to find %@ after %f seconds",element,timeout];
            [self recordFailureWithDescription:message inFile:file atLine:line expected:YES];
        }
    }];
}

USAGE

XCUIElement *element = app.staticTexts["Name of your element"];
[self waitForElementToAppear:element withTimeout:5];

Swift

func waitForElementToAppear(element: XCUIElement, timeout: NSTimeInterval = 5,  file: String = #file, line: UInt = #line) {
        let existsPredicate = NSPredicate(format: "exists == true")

        expectationForPredicate(existsPredicate,
                evaluatedWithObject: element, handler: nil)

        waitForExpectationsWithTimeout(timeout) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find \(element) after \(timeout) seconds."
                self.recordFailureWithDescription(message, inFile: file, atLine: line, expected: true)
            }
        }
    }

USAGE

let element = app.staticTexts["Name of your element"]
self.waitForElementToAppear(element)

or

let element = app.staticTexts["Name of your element"]
self.waitForElementToAppear(element, timeout: 10)

SOURCE

Ricardo answered 22/11, 2015 at 13:21 Comment(3)
looking for some more illustration regarding above xcode9 exampleUnpeople
you may check shashikantjagtap.net/asynchronous-ios-testing-swift-xcwaiterRicardo
Tested. Works like a charm! Thanks!Measurement
D
38

As of Xcode 8.3, we can use XCTWaiter http://masilotti.com/xctest-waiting/

func waitForElementToAppear(_ element: XCUIElement) -> Bool {
    let predicate = NSPredicate(format: "exists == true")
    let expectation = expectation(for: predicate, evaluatedWith: element, 
                                  handler: nil)

    let result = XCTWaiter().wait(for: [expectation], timeout: 5)
    return result == .completed
}

Another trick is to write a wait function, credit goes to John Sundell for showing it to me

extension XCTestCase {

  func wait(for duration: TimeInterval) {
    let waitExpectation = expectation(description: "Waiting")

    let when = DispatchTime.now() + duration
    DispatchQueue.main.asyncAfter(deadline: when) {
      waitExpectation.fulfill()
    }

    // We use a buffer here to avoid flakiness with Timer on CI
    waitForExpectations(timeout: duration + 0.5)
  }
}

and use it like

func testOpenLink() {
  let delegate = UIApplication.shared.delegate as! AppDelegate
  let route = RouteMock()
  UIApplication.shared.open(linkUrl, options: [:], completionHandler: nil)

  wait(for: 1)

  XCTAssertNotNil(route.location)
}
Decrescent answered 14/2, 2017 at 9:28 Comment(0)
Q
22

This will create a delay without putting the thread to sleep or throwing an error on timeout:

let delayExpectation = XCTestExpectation()
delayExpectation.isInverted = true
wait(for: [delayExpectation], timeout: 5)

Because the expectation is inverted, it will timeout quietly.

Quarterdeck answered 28/5, 2020 at 0:15 Comment(0)
R
14

Based on @Ted's answer, I've used this extension:

extension XCTestCase {

    // Based on https://stackoverflow.com/a/33855219
    func waitFor<T>(object: T, timeout: TimeInterval = 5, file: String = #file, line: UInt = #line, expectationPredicate: @escaping (T) -> Bool) {
        let predicate = NSPredicate { obj, _ in
            expectationPredicate(obj as! T)
        }
        expectation(for: predicate, evaluatedWith: object, handler: nil)

        waitForExpectations(timeout: timeout) { error in
            if (error != nil) {
                let message = "Failed to fulful expectation block for \(object) after \(timeout) seconds."
                let location = XCTSourceCodeLocation(filePath: file, lineNumber: line)
                let issue = XCTIssue(type: .assertionFailure, compactDescription: message, detailedDescription: nil, sourceCodeContext: .init(location: location), associatedError: nil, attachments: [])
                self.record(issue)
            }
        }
    }

}

You can use it like this

let element = app.staticTexts["Name of your element"]
waitFor(object: element) { $0.exists }

It also allows for waiting for an element to disappear, or any other property to change (by using the appropriate block)

waitFor(object: element) { !$0.exists } // Wait for it to disappear
Roumell answered 12/1, 2017 at 10:34 Comment(3)
+1 very swifty, and it uses the block predicate which I think is a lot better because the standard predicate expressions didn't work for me sometimes, for example when waiting for some properties on XCUIElements etc.Passover
For Xcode 13.1 just change line: UInt = #line => line: Int = #line. There are compiler erros that basically require this fixArcature
Nice catch @SevenDays, lineNumber must be an Int now.Helianthus
C
13

Xcode testing Wait

In my case sleep created a side effect so I used wait

let _ = XCTWaiter.wait(for: [XCTestExpectation(description: "Hello World!")], timeout: 2.0)
Conversant answered 24/5, 2019 at 15:3 Comment(0)
R
10

Edit:

It actually just occurred to me that in Xcode 7b4, UI testing now has expectationForPredicate:evaluatedWithObject:handler:

Original:

Another way is to spin the run loop for a set amount of time. Really only useful if you know how much (estimated) time you'll need to wait for

Obj-C: [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow: <<time to wait in seconds>>]]

Swift: NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: <<time to wait in seconds>>))

This is not super useful if you need to test some conditions in order to continue your test. To run conditional checks, use a while loop.

Ravelin answered 22/7, 2015 at 13:9 Comment(1)
This is clean and very useful for me especially e.g waiting for app launch, request preloaded data and do login / logout stuffs. Thank you.Lampedusa
M
9

How we're doing it at my current company is we create an XCUIElement expression expectation (to create a versatile wait method). We do it the following way to make sure it's maintainable (lots of expectation variety, and don't want to create lots of methods/specific predicates to do so.

Swift 5

Base method

The expression is used to form a dynamic predicate value. We can create XCTNSPredicateExpectation's from predicates, which we then pass into XCTWaiter to explicitly wait. If the result was anything other than completed, then we fail with an optional message.

@discardableResult
func wait(
    until expression: @escaping (XCUIElement) -> Bool,
    timeout: TimeInterval = 15,
    message: @autoclosure () -> String = "",
    file: StaticString = #file,
    line: UInt = #line
) -> Self {
    if expression(self) {
        return self
    }

    let predicate = NSPredicate { _, _ in
        expression(self)
    }

    let expectation = XCTNSPredicateExpectation(predicate: predicate, object: nil)

    let result = XCTWaiter().wait(for: [expectation], timeout: timeout)

    if result != .completed {
        XCTFail(
            message().isEmpty ? "expectation not matched after waiting" : message(),
            file: file,
            line: line
        )
    }

    return self
}

Usage

app.buttons["my_button"].wait(until: { $0.exists })
app.buttons["my_button"].wait(until: { $0.isHittable })

Keypaths

Then we wrap that in a method where a keyPath and match value form the expression.

@discardableResult
func wait<Value: Equatable>(
    until keyPath: KeyPath<XCUIElement, Value>,
    matches match: Value,
    timeout: TimeInterval = 15,
    message: @autoclosure () -> String = "",
    file: StaticString = #file,
    line: UInt = #line
) -> Self {
    wait(
        until: { $0[keyPath: keyPath] == match },
        timeout: timeout,
        message: message,
        file: file,
        line: line
    )
}

Usage

app.buttons["my_button"].wait(until: \.exists, matches: true)
app.buttons["my_button"].wait(until: \.isHittable, matches: false)

Then you can wrap that method, where the match value is always true for a use case I found most common.

Usage

app.buttons["my_button"].wait(until: \.exists)
app.buttons["my_button"].wait(until: \.isHittable)

I wrote a post about it, and get the full extension file there too: https://sourcediving.com/clean-waiting-in-xcuitest-43bab495230f

Menderes answered 27/3, 2021 at 19:38 Comment(0)
R
4

The following code just works with Objective C.

- (void)wait:(NSUInteger)interval {

    XCTestExpectation *expectation = [self expectationWithDescription:@"wait"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(interval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });
    [self waitForExpectationsWithTimeout:interval handler:nil];
}

Just make call to this function as given below.

[self wait: 10];
Rather answered 9/5, 2017 at 10:59 Comment(5)
Error --> caught "NSInternalInconsistencyException", "API violation - call made to wait without any expectations having been set."Giralda
@iOSCalendarpatchthecode.com, Have you found alternate solution for that?Picul
@Picul can you use any of the others on this page?Giralda
@iOSCalendarpatchthecode.com No, I just need some delay without any element to check. So I need alternate of this.Picul
@Picul i used the selected answer on this page. It worked for me. Maybe you can ask them what specifically you are looking for.Giralda
S
2

sleep will block the thread

"No run loop processing occurs while the thread is blocked."

you can use waitForExistence

let app = XCUIApplication()
app.launch()

if let label = app.staticTexts["Hello, world!"] {
label.waitForExistence(timeout: 5)
}
Slaughterhouse answered 19/5, 2020 at 17:33 Comment(0)
E
2
   let app = XCUIApplication()
    app.launch()

     //Find the button in the UI 
    let SettingsButton =
        app.navigationBars["HomeView"].buttons["Settings"]
    XCTAssertTrue(settingButton.waitForExistence(timeout: 10))
Erie answered 8/9, 2021 at 10:4 Comment(0)
E
0

According to the API for XCUIElement .exists can be used to check if a query exists or not so the following syntax could be useful in some cases!

let app = XCUIApplication()
app.launch()

let label = app.staticTexts["Hello, world!"]
while !label.exists {
    sleep(1)
}

If you are confident that your expectation will be met eventually you could try running this. It should be noted that crashing might be preferable if the wait is too long in which case waitForExpectationsWithTimeout(_,handler:_) from @Joe Masilotti's post should be used.

Echols answered 18/7, 2016 at 20:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.