XCTestCase - iOS UI Tests - dealing with UITableViews with many cells
Asked Answered
U

2

18

I am experimenting with the (Xcode 7) UI XCTestCase test cases and I just stumbled onto an issue with one UIView, in which I have a UITableView with many cells(4000+).

When the app is running normally, only the visible cells are rendered and there is no performance issue at all. However, if I run the app within the context of recording a XCTestCase and I navigate to this screen, the simulator freezes, apparently because each single cell is rendered as if it were visible. If I try to script the navigation manually and I run the XCTestCase, the test case fails right after navigating to this screen, exiting with a "UI Testing Failure - Failed to get refreshed snapshot", apparently again because all cells are being rendered and this does not finish in time.

I think this has to do with the fact that the testing framework builds an entire metamodel of the screen under display, adding each of the 4000+ cells into the view tree hierarchy.

I tried adding an expectation, hoping this would give the testing container enough time to finish rendering all cells, but this does not work.

Is there a workaround for this? Is it somehow possible to skip building part of the UI tree hierarchy or something? My goal is being able to write UI tests for this screen.

Unsnap answered 4/11, 2015 at 18:30 Comment(2)
Could you please show your test code? ThksCapello
It is a custom cell with 3 labels and 2 images. I managed to workaround it by using [tableView indexPathsForVisibleRows]; to determine that the cell is visible and if it is not returning [UITableViewCell new] for my tests, so the used cell is simple enough, but this is not what I am looking for. I am thinking towards somehow letting the user interface tree be built lazily, but I don't think this is possible right now. Would be nice if these xc ui test cases could be configured to not have such serious side effects(such as the loading of all cells in this case).Unsnap
D
6

You might be able to avoid having the entire table render, if you can use firstMatch instead of element, and also avoid count.

I had a test that checks for expected labels in the first two cells of a table. At first, I was using app.table.cells.element(boundBy: 0) and app.table.cells.element(boundBy: 1) to find the first and second cells. This was resulting in the whole table being rendered before I could access the cells.

I adapted my test to be slightly less precise, but still good enough for me (given the huge amount of time it would take otherwise). Instead, I use matching with predicates on the expected label values, with firstMatch, to find the first cells matching the criteria I want. This way the traversal stops as soon as it finds them (and since they're at the top of the table, it's quick).

Here's the code before and after.

Before (slow, yet more precise):

private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
    let table = app.tables.element
    let cell0 = table.cells.element(boundBy: 0)
    let cell1 = table.cells.element(boundBy: 1)
    let actualRhyme0 = cell0.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
    let actualRhyme1 = cell1.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label

    XCTAssertEqual(expectedFirstRhyme, actualRhyme0, "Expected first rhyme for \(query) to be \(expectedFirstRhyme) but found \(actualRhyme0)")
    XCTAssertEqual(expectedSecondRhyme, actualRhyme1, "Expected first rhyme for \(query) to be \(expectedSecondRhyme) but found \(actualRhyme1)")
}

Faster, but less precise (but good enough):

private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
    let table = app.tables.firstMatch
    let label0 = table.cells.staticTexts.matching(NSPredicate(format: "label = %@", expectedFirstRhyme)).firstMatch
    let label1 = table.cells.staticTexts.matching(NSPredicate(format: "label = %@", expectedSecondRhyme)).firstMatch

    // We query for the first cells that we find with the expected rhymes,
    // instead of directly accessing the 1st and 2nd cells in the table,
    // for performance issues.
    // So we can't add assertions for the "first" and "second" rhymes.
    // But we can at least add assertions that both rhymes are visible,
    // and the first one is above the second one.
    XCTAssertTrue(label0.frame.minY < label1.frame.minY)
    XCTAssertTrue(label0.isHittable)
    XCTAssertTrue(label1.isHittable)
}

Reference: https://developer.apple.com/documentation/xctest/xcuielementquery/1500515-element

Use the element property to access a query’s result when you expect a single matching element for the query, but want to check for multiple ambiguous matches before accessing the result. The element property traverses your app’s accessibility tree to check for multiple matching elements before returning, and fails the current test if there is not a single matching element.

In cases where you know categorically that there will be a single matching element, use the XCUIElementTypeQueryProvider firstMatch property instead. firstMatch stops traversing your app’s accessibility hierarchy as soon as it finds a matching element, speeding up element query resolution.

Delois answered 31/10, 2018 at 23:16 Comment(1)
This solution works for getting attribute like .label, .frame, .exists, etc. But it does not work when the element is tapped, or any other situation when the intention is more than getting a property.Braun
X
1

I had the same issue, and I agree it is frustrating having to wait for the entire table to load, but that is what I had to do using the following workaround.

This may not be what you are looking for but it may help others:

Basically I am counting the cells in the table 2 times consecutively if they are not equal that means the table is still loading. Put it in a loop it and do that until both counts return the same number which would mean the table is finished loading. I then put in a stop of 30 seconds so that if this takes longer than 30 seconds, the test will fail (this was enough time in my case). If your table will take longer than that you could increase the number to 180 for 3 mins etc...

    let startTime = NSDate()
    var duration : TimeInterval
    var cellCount1 : UInt = app.tables.cells.count
    var cellCount2 : UInt = app.tables.cells.count
    while (cellCount1 != cellCount2) {
        cellCount1 = app.tables.cells.count
        cellCount2 = app.tables.cells.count
        duration = NSDate().timeIntervalSince(startTime as Date)
        if (duration > 30) {
            XCTFail("Took too long waiting for cells to load")
        }
    }
    //Now I know the table is finished loading and I can tap on a cell
    app.tables.cells.element(boundBy: 1).tap()
Xavier answered 8/2, 2016 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.