在Xcode UI测试中,如何在测试用例中加入延迟/等待时间?

226

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

在继续执行后续步骤之前,是否有一种方法可以在XCTestCase中引入延迟或等待机制?

目前没有适当的文档可用,并且我已经查看了类的头文件。未能找到与此相关的任何内容。

有什么想法/建议吗?


14
我认为 NSThread.sleepForTimeInterval(1) 应该有效。 - Kametrixom
太好了!看起来这个可行。但我不确定这是否是推荐的方法。我认为苹果应该提供更好的方法来解决这个问题。可能需要提交一个Radar。 - Tejas HS
我认为这样做其实很好,这是暂停当前线程一定时间内最常用的方法。如果您想要更多的控制,还可以使用GCD(即“dispatch_after”、“dispatch_queue”等)。 - Kametrixom
@Kametrixom 不要勾选运行循环 - 苹果在Beta 4中引入了本地异步测试。有关详细信息,请参见我的答案 - Joe Masilotti
3
Swift 4.0 --> Thread.sleep(forTimeInterval: 2) - uplearned.com
14个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
254

此外,您可以仅仅睡觉:

sleep(10)

由于UITests在另一个进程中运行,所以这个方法可行。我不知道它是否可取,但它有效。


2
有时我们需要延迟一些操作,但又不希望它引发错误!谢谢 - Tai Le
11
我喜欢NSThread.sleepForTimeInterval(0.2)因为你可以指定小于一秒的延迟时间。(sleep()需要一个整数参数,只能使用秒的倍数)。 - Graham Perks
6
@GrahamPerks,是的,不过也有 usleep - mxcl
2
这真的是一个很糟糕的建议。如果你有一些这样的测试用例,你的集成测试将会无休止地持续下去。 - CouchDeveloper
3
这不是一个糟糕的建议(你可能不了解UITesting如何工作),但即使它是糟糕的建议,有时候也没有办法设计一个有效的预期(系统警报会造成干扰),所以这就是唯一的选择。 - mxcl
显示剩余6条评论

193

在 Xcode 7 Beta 4 中引入了异步 UI 测试。要等待一个文本为“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)

关于UI测试的更多细节可以在我的博客上找到。


23
很遗憾,无法接受超时发生的事实并继续执行 - waitForExpectationsWithTimeout 会自动使您的测试失败,这非常不幸。 - Jedidja
@Jedidja 实际上,这在我的 XCode 7.0.1 上并没有发生。 - Bastian
@Bastian 嗯,有趣;我得重新检查一下。 - Jedidja
1
它对我不起作用。这是我的示例:let xButton = app.toolbars.buttons["X"] let exists = NSPredicate(format: "exists == 1") expectationForPredicate(exists, evaluatedWithObject: xButton, handler: nil) waitForExpectationsWithTimeout(10, handler: nil) - emoleumassi
这种方法太复杂了,天啊,希望我没看过它。 - MartianMartian
显示剩余2条评论

102

iOS 11 / Xcode 9

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

这是对该网站所有自定义实现的一个很好的替代品!

一定要看看我在这里的回答:https://dev59.com/WKnka4cB1Zd3GeqPLkgh#48937714。在那里,我描述了一种等待请求的替代方案,可以大大减少测试运行的时间!


谢谢@daidai,我已经更改了文本 :) - blackjacx
2
是的,当我使用“XCTestCase”时,这仍然是我采用的方法,而且它运作得非常好。我不明白为什么像“sleep(3)”这样的方法在这里得到了如此高的评价,因为它会人为地延长测试时间,并且当你的测试套件变大时,这确实不是一个选择。 - blackjacx
实际上需要 Xcode 9,但也可以在运行 iOS 10 的设备/模拟器上工作 ;-) - d4Rk
是的,我在标题上写了那个。但现在大多数人应该已经升级到至少Xcode 9了;-) - blackjacx
9
我的回答之所以被投票得那么高,是因为我九年前写了这篇回答,当时这是唯一的选择。在某些情况下,它仍然是唯一的选择。你并不总是有元素可等待其存在。我给了你一个赞,希望这能让你冷静一些。 - mxcl

79

Xcode 9 引入了使用 XCTWaiter 进行测试的新技巧。

测试用例显式等待。

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

服务员实例委托给测试

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

服务员类返回结果

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
}

使用示例

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

使用方法

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)

来源


1
寻找更多关于上述Xcode9示例的说明。 - rd_
请查看网址http://shashikantjagtap.net/asynchronous-ios-testing-swift-xcwaiter/的相关编程内容。 - Ted
1
测试完成,运行得非常好!谢谢! - Dawid Koncewicz

37

自 Xcode 8.3 起,我们可以使用 XCTWaiterhttp://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)
}

21

这将在不使线程进入睡眠状态或在超时时引发错误的情况下创建延迟:

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

由于预期被反转,它将会静默超时。


14

根据@Ted的回答,我已经使用了这个扩展程序:

extension XCTestCase {

    // Based on https://dev59.com/BV0Z5IYBdhLWcg3wrR8r#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)
            }
        }
    }

}

您可以像这样使用它

let element = app.staticTexts["Name of your element"]
waitFor(object: element) { $0.exists }
它还允许等待元素消失,或者通过使用适当的块来更改任何其他属性。
waitFor(object: element) { !$0.exists } // Wait for it to disappear

+1 非常流畅,而且它使用块谓词,我认为这更好,因为标准的谓词表达式有时对我不起作用,例如当等待 XCUIElements 上的某些属性时。 - lawicko
1
对于 Xcode 13.1,只需要更改以下代码行:UInt = #line => Int = #line。有一些编译器错误基本上需要这个修复。 - SevenDays
1
不错的发现 @SevenDays,lineNumber 现在必须是 Int 类型。 - Mishka

12

Xcode测试等待

在我的情况下,sleep 会产生副作用,所以我使用了 wait

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

10

编辑:

在 Xcode 7b4 中,UI测试现在具有expectationForPredicate:evaluatedWithObject:handler:方法。

原始内容:

另一种方法是旋转运行循环一段时间。只有在您知道需要等待多长时间时才真正有用。

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

如果需要测试一些条件以便继续测试,则此方法并不是特别有用。要运行条件检查,请使用while循环。


这对我来说非常干净且有用,特别是等待应用程序启动、请求预加载数据以及执行登录/注销操作。谢谢。 - felixwcf

9
在我目前的公司,我们创建了一个XCUIElement表达式期望(以创建一个通用的等待方法)。我们按照以下方式进行操作,以确保其可维护性(有很多期望变化,不想创建很多方法/特定谓词来完成)。 基本方法是使用表达式来形成动态谓词值。我们可以从谓词创建XCTNSPredicateExpectation,然后将其传递给XCTWaiter进行显式等待。如果结果不是“已完成”,则我们会失败并提供一个可选消息。 Swift 5
@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: { $0.exists })
app.buttons["my_button"].wait(until: { $0.isHittable })

关键路径

然后我们将其包装在一个方法中,其中关键路径和匹配值形成表达式。

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

使用方法

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)

我写了一篇关于这个的文章,你可以在那里找到完整的扩展文件:https://sourcediving.com/clean-waiting-in-xcuitest-43bab495230f


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,