Delay/Wait 在 Xcode UI 测试的测试用例中

Delay/Wait in a test case of Xcode UI testing

我正在尝试使用 Xcode 7 beta 2 中可用的新 UI 测试编写测试用例。该应用程序有一个登录屏幕,它会调用服务器进行登录。由于它是异步操作,因此会有延迟。

在继续下一步之前,是否有办法在 XCTestCase 中引起延迟或等待机制?

没有合适的文档可用,我查看了 类 的头文件。找不到与此相关的任何内容。

任何ideas/suggestions?

编辑:

实际上我刚刚想到,在 Xcode 7b4 中,UI 测试现在有 expectationForPredicate:evaluatedWithObject:handler:

原文:

另一种方法是让 运行 循环旋转一段设定的时间。只有当你知道你需要等待多少(估计)时间时才真正有用

目标-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>>))

如果您需要测试某些条件以继续测试,这不是很有用。要 运行 条件检查,请使用 while 循环。

异步 UI 测试是在 Xcode 7 Beta 4 中引入的。要等待带有文本 "Hello, world!" 的标签出现,您可以执行以下操作:

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)

更多details about UI Testing可以在我的博客上找到。

Xcode 9 XCTWaiter

引入了新技巧

测试用例显式等待

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

Waiter实例委托测试

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

服务员classreturns结果

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

之前Xcode9

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];
        }
    }];
}

用法

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)
            }
        }
    }

用法

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

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

SOURCE

另外,你可以睡觉了:

sleep(10)

由于 UITests 运行 在另一个进程中,所以这行得通。我不知道这有多明智,但它确实有效。

根据 XCUIElement 的 API .exists 可用于检查查询是否存在,因此以下语法在某些情况下可能很有用!

let app = XCUIApplication()
app.launch()

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

如果您确信您的期望最终会得到满足,您可以尝试 运行 这个。应该注意的是,如果等待时间过长,崩溃可能更可取,在这种情况下,应使用来自@Joe Masilotti 的 post 的 waitForExpectationsWithTimeout(_,handler:_)

基于,我使用了这个扩展:

extension XCTestCase {

    // Based on 
    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)
            }
        }
    }

}

你可以这样使用它

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

它还允许等待元素消失,或任何其他 属性 改变(通过使用适当的块)

waitFor(object: element) { ![=12=].exists } // Wait for it to disappear

从 Xcode 8.3 开始,我们可以使用 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
}

另一个技巧是编写一个 wait 函数,感谢 John Sundell 向我展示了它

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)
  }
}

并像

一样使用它
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)
}

以下代码仅适用于 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];
}

只需按如下所示调用此函数。

[self wait: 10];

iOS 11 / Xcode 9

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

这是此站点上所有自定义实现的绝佳替代品!

请务必在此处查看我的回答:。在那里我描述了等待请求的替代方法,这将大大减少您的测试时间 运行!

Xcode 测试等待

在我的例子中,sleep 产生了副作用,所以我使用了 wait

let _ = XCTWaiter.wait(for: [XCTestExpectation(description: "Hello World!")], timeout: 2.0)

睡眠会阻塞线程

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

你可以使用 waitForExistence

let app = XCUIApplication()
app.launch()

if let label = app.staticTexts["Hello, world!"] {
label.waitForExistence(timeout: 5)
}

这将创建一个延迟,而不会使线程进入睡眠状态或在超时时抛出错误:

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

因为期望是倒置的,所以会悄悄超时

我现在的公司是如何做的,我们创建了一个 XCUIElement 表达式期望(以创建一个通用的等待方法)。我们通过以下方式来确保它是可维护的(很多期望变化,并且不想创建很多 methods/specific 谓词来这样做。

Swift 5

基本方法

表达式用于形成动态谓词值。我们可以从谓词创建 XCTNSPredicateExpectation,然后将其传递给 XCTWaiter 以显式等待。如果结果不是 completed,那么我们会失败并显示一条可选消息。

@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
}

用法

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

键路径

然后我们将其包装在一个方法中,其中 keyPath 和 match 值构成表达式。

@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: { [=12=][keyPath: keyPath] == match },
        timeout: timeout,
        message: message,
        file: file,
        line: line
    )
}

用法

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

然后您可以包装该方法,其中 match 值始终是 true 对于我发现最常见的用例。

用法

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

我写了一个关于它的 post,并且在那里也得到了完整的扩展文件:https://sourcediving.com/clean-waiting-in-xcuitest-43bab495230f

   let app = XCUIApplication()
    app.launch()

     //Find the button in the UI 
    let SettingsButton =
        app.navigationBars["HomeView"].buttons["Settings"]
    XCTAssertTrue(settingButton.waitForExistence(timeout: 10))