Delay/Wait in a test case of Xcode UI testing
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.
Wait 30 sec in Xcode UI Test
The simplest way is just sleeping the execution for the time being:
sleep(30)
However, in case you are expecting something to appear, it's better to use the built-in function for waiting for existence:
element.waitForExistence(30)
It doesn't fail if nothing appears, so if it's crucial part of your logic, you probably better to check it via expectation with timer:
let exists = NSPredicate(format: "exists == %@", true)
expectationForPredicate(exists, evaluatedWithObject: element, handler: nil)
waitForExpectationsWithTimeout(30, handler: nil)
How to wait in a XCTest for T seconds without timeout error?
You can use XCTWaiter.wait functions.
For example:
let exp = expectation(description: "Test after 5 seconds")
let result = XCTWaiter.wait(for: [exp], timeout: 5.0)
if result == XCTWaiter.Result.timedOut {
XCTAssert(<test if state is correct after this delay>)
} else {
XCTFail("Delay interrupted")
}
How to wait for UI element without failing test case
You will have to reimplement waiting. As terrible as it sounds, I recommend using Sleep and If statements; you want to lock your test thread while it waits for the app to finish presenting content, then evaluate.
Unfortunately, as far as I'm aware none of the options for waitForExpectationsWithTimeout
permit you to not fail the test if the expectationForPredicate is not fulfilled at the end of the timeout time.
Delay in unit test
I have two suggestions to use together:
- Use a spy test double to make sure that the service your data store uses to refresh the clinics is called twice
- Inject the refresh interval to make the tests faster
Spy test double
Testing the side effect of the data loading, that it hits the service, could be a way to simplify your test.
Instead of using different expectations and exercising the system under test in a way that might not be what happens at runtime (the dataStore.clinicsSignal.fire([])
) you can just count how many times the service is hit, and assert the value is 2.
Inject refresh interval
The approach I would recommend is to inject the time setting for how frequently the clinics should be updated in the class, and then set a low value in the tests.
After all, I'm guessing what you are interested in is that the update code runs as expected, not every 10 seconds. That is, it should update at the frequency you set.
You could do this by having the value as a default in the init of your data store, and then override it in the tests.
The reason I'm suggesting to use a shorter refresh interval is that in the context of unit testing, the faster they run the better it is. You want the feedback loop to be as fast as possible.
Putting it all together, something more or less like this
protocol ClinicsService {
func loadClinics() -> SignalProducer<[Clinics], ClinicsError>
}
class DataSource {
init(clinicsService: ClinicsService, refreshInterval: TimeInterval = 5) { ... }
}
// in the tests
class ClinicsServiceSpy: ClinicsService {
private(var) callsCount: Int = 0
func loadClinics() -> SignalProducer<[Clinics], ClinicsError> {
callsCount += 1
// return some fake data
}
}
func testRefresh() {
let clinicsServiceSpy = ClinicsServiceSpy()
let dataStore = DataStore(clinicsService: clinicsServiceSpy, refreshInterval: 0.05)
// This is an async expectation to make sure the call count is the one you expect
_ = expectation(
for: NSPredicate(
block: { input, _ -> Bool in
guard let spy = input as? ClinicsServiceSpy else { return false }
return spy.callsCount == 2
),
evaluatedWith: clinicsServiceSpy,
handler: .none
)
dataStore.loadData()
waitForExpectations(timeout: .2, handler: nil)
}
If you also used Nimble to have a more refined expectation API your test could look like this:
func testRefresh() {
let clinicsServiceSpy = ClinicsServiceSpy()
let dataStore = DataStore(clinicsService: clinicsServiceSpy, refreshInterval: 0.05)
dataStore.loadData()
expect(clinicsServiceSpy.callsCount).toEventually(equal(2))
}
The tradeoff you make in this approach is to make the test more straightforward by writing a bit more code. Whether it's a good tradeoff is up to you do decide.
I like working in this way because it keeps each component in my system free from implicit dependencies and the test I end up writing are easy to read and work as a living documentation for the software.
Let me know what you think.
Swift2 UI Test - Wait for Element to Appear
In expectationForPredicate(predicate: evaluatedWithObject: handler:)
you don't give an actual object, but a query to find it in the view hierarchy. So, for example, this is a valid test:
let predicate = NSPredicate(format: "exists == 1")
let query = XCUIApplication().buttons["Button"]
expectationForPredicate(predicate, evaluatedWithObject: query, handler: nil)
waitForExpectationsWithTimeout(3, handler: nil)
Check out UI Testing Cheat Sheet and documentation generated from the headers (there is no official documentation at the moment), all by Joe Masilotti.
Wait for all HTTP requests to finish in XCode UI tests?
Your best bet is to wait for some UI element to appear or disappear. Think of it this way:
The framework acts like a user. It doesn't care what code is running under the hood. The only thing that matters is what is visible on the screen.
That said, here is how you can wait for a label titled "Go!" to appear in your UI Tests.
let app = XCUIApplication()
let goLabel = self.app.staticTexts["Go!"]
XCTAssertFalse(goLabel.exists)
let exists = NSPredicate(format: "exists == true")
expectationForPredicate(exists, evaluatedWithObject: goLabel, handler: nil)
app.buttons["Ready, set..."].tap()
waitForExpectationsWithTimeout(5, handler: nil)
XCTAssert(goLabel.exists)
You could also extract that into a helper method. If you use some Swift compiler magic you can even get the failure message to occur on the line that called the method.
private fund waitForElementToAppear(element: XCUIElement, file: String = #file, line: UInt = #line) {
let existsPredicate = NSPredicate(format: "exists == true")
expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)
waitForExpectationsWithTimeout(5) { (error) -> Void in
if (error != nil) {
let message = "Failed to find \(element) after 5 seconds."
self.recordFailureWithDescription(message, inFile: file, atLine: line, expected: true)
}
}
}
Wait for NSNotification before continuing
Effective UI Tests are written with a different mindset than unit tests and production code. UI Tests should be written from the user's perspective and not care what goes on "under the hood" of the app. For example, your test should wait for a specific UI element to appear instead of waiting for an NSNotification to fire or a specific network request to complete.
Wait for An Element to Appear
Let's say your first tab/screen displays user information. Contained in that data is the user's username. You can use XCTest's asynchronous testing API to pause the framework until the element is found (or a timeout occurs).
let app = XCUIApplication()
let username = self.app.staticTexts["joemasilotti"]
let exists = NSPredicate(format: "exists == true")
expectationForPredicate(exists, evaluatedWithObject: username, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)
XCTAssert(username.exists)
This comes directly from a UI Testing cheat sheet I put together. It also has a working example app that you can run in Xcode and play around with the code and tests.
Related Topics
Self Scrollable Text Inside Cascrolllayer
How to Animate Add Uisearchbar on Top of Uinavigationbar
Sending Push Notifications to iOS from Pwa
Uiwindow Not Showing Over Content in iOS 13
How to Find a Specification in Cocoapods
Load Image from Bundle with iOS
How to Track User Location in Background
How to Change the Highlighted Color of a Uibutton
Ios7 Uipickerview How to Hide the Selection Indicator
Adjust Font Size of Text to Fit in Uibutton
How to Run Timer Though the App Entered Background or Is Terminated
Detected a Case Where Constraints Ambiguously Suggest a Height of Zero
Set Uitableview Content Inset Permanently
Ios: Uiview Subclass Init or Initwithframe:
iOS App Crashing Every Other Launch, Can't Find Error
A Launch Storyboard or Xib Must Be Provided Unless the App Requires Full Screen