Support toolkit for tests
A support library for interfacing between app and tests.
Represents a string value passed to the app from a UI test. Extend this type with new constants to pass them to the app.
MyAppTestSupport package:
extension LaunchEnv {
static let myEnvValue
}UI Tests:
import XCTestSupport
XCUIApplication().setLaunchEnv(.myEnvValue, "example")App:
if let value = LaunchEnv.myEnvValue.value {
// ...
}Represents a boolean value passed to the app from a UI test. Extend this type with new constants to pass them to the app.
MyAppTestSupport package:
extension LaunchArg {
static let myArg
}UI Tests:
import XCTestSupport
XCUIApplication().addLaunchArg(.myArg)App:
if LaunchArg.myArg.value {
// ...
}Test-specific extensions and helpers.
UI tests can achieve code reuse using the Robots pattern. ‘Robots’ interact with particular pages, modes, or configurations of the app, on behalf of the test, keeping the test definition super simple.
XCTestSupport includes the Robot protocol, and related protocols such as PushedRobotProtocol.
Robots represent a page in the app. Robots can make assertions about a displayed page in the app, based on data provided by the test. Robots can also navigate, returning new robots representing a new page or distinct configuration.
Anatomy of a UI test making use of Robots:
AppRobot() // Start the chain by creating the robot representing the app being tested.
// The app launches. We start on the login screen.
.login() // Proceeds from the login screen. Return the Home Screen Robot.
// We are now on the home screen.
// The Home Screen Robot has been initalised and
// performs a wait to ensure the view displays.
// Autocomplete for `.` now suggests actions that can be performed on the homescreen.
.showUserMenu() // Taps the menu button and returns a Menu Robot.
.checkUsername(matches: "Bobby Tables")
// Performs a check on the content of the menu.
// Note that the dynamic data - the username - is passed in,
// rather than encoded into the robot. This ensures that
// checkUsername can be reused in other tests.
.closeUserMenu()
// Menu Robot remembers it was presented from Home Screen, and returns Home Screen Robot.Full implementation example:
struct HomeScreenRobot: Robot {
init() {
// Wait for an element to appear
}
// MARK: - Elements
var myButton: XCUIElement {
app.buttons["My Button ID or title"]
}
// MARK: - Validation
@discardableResult
func checkButtonIsShown(expectedTitle: String? = nil) -> Self {
XCTAssert(myButton.exists)
if let expectedTitle {
XCTAssertEqual(myButton.value as? String, expectedTitle)
}
return self
}
// MARK: - Navigation
@discardableResult
func showDetail() -> DetailRobot<Self> { // `Self` is passed as the Parent robot.
myButton.tap()
return DetailRobot()
}
}
/// Example Detail Robot inheriting from PushedRobot
/// PushedRobot uses a Parent generic to implement navigation operations for a pushed View, such as `.goBack()` and `.checkCanGoBack()`.
struct DetailRobot<Parent: Robot>: PushedRobot {
// ...
}
func myTest() {
AppRobot()
.login() // Returns HomeScreenRobot.
.checkButtonIsShown(expectedTitle: "Hello world")
.showDetail() // Returns DetailRobot, which can make assertions that Detail is shown in its init.
.goBack() // Returns HomeScreenRobot, because HomeScreenRobot set itself as the parent.
}Configure launch parameters for the app when used in UI tests. See the section under TestSupport above for usage examples.
This package defines helpers for passing NetMock parameters via launch arguments.
extension AppRobot {
@discardableResult
func failLogin() -> Self {
// Various alternatives:
app.netmockOverride(.GET, "https://api.example.com/login", response: "Failure")
app.netmockOverride("https://api.example.com/login", response: "Failure")
app.netmockOverride("https://api.example.com/login", responses: ["Failure", "Success"])
let failLoginOverride = NetMock.Override(method: .GET, url: URL(string: "https://api.example.com/login")!, responses: ["Failure"])
app.netmockOverride(failLoginOverride)
return self
}
}An uninstall helper is provided on XCUIApplication which deletes the app from the HomeScreen via touch interactions. If an app is able to tear down its state programmatically, that approach is preferred as it is significantly faster.
app.uninstall()A wait helper is provided, which accepts either a KeyPath or a closure producing a Bool, and an optional custom timeout.
This can be more flexible than the built-in wait function recently introduced, which only accepts a KeyPath.
button.wait(for: \.isHittable)
page.wait(for: \.exists, timeout: 10)
text.wait(for: { $0.value as? String == "Hello world" })