XCUITest — Test Automation Organization

Ronell Lukasik
4 min readNov 2, 2021

Over the past several years we have made adjustments to our test case file structure and files to improve maintainability and readability. There are several ways you can organize your test files but I wanted to discuss our current approach and how it has helped us. Hopefully you can find these ideas beneficial to your project as well.

Folder Structure

We break our test files apart into the following folders shown above: Elements, Functions, Stubs, Tests.

  • Tests: this is where the actual test files are stored. If a Business Analyst, Product Owner, or another team member wants to review the test cases and expected results, this is where they go. We additionally break down this folder further by making subfolders for the different areas of the application making it easier to find the exact test files you are looking for.
  • Functions: this is where are page/screen objects are stored and also broken down into subfolders for the different areas of the application. These files contain the functions that interact with the screen/objects, perform verifications, wait for elements, or contain element functions (more on this later). Modularizing this code into separate test files really help to improve the maintainability and readability of the test files. BA’s or other team members (outside of QA) typically won’t care how we go about performing the verification of the scenarios, but they want to be able to easily read the test files to confirm the scenario performed and expected results.
  • Stubs: Depending on if you are using stub data for your automation and what kind of framework you are using, you may or may not need this. We currently use https://github.com/Subito-it/SBTUITestTunnel for our stubbing solution, so our stub files are managed within this folder. This article will not cover this stubbing framework; however, if you are interested you can check out this article instead https://rlukasik.medium.com/xcuitest-stubbing-network-calls-c04cf69ddb7f
  • Elements: a.k.a Locators. These files contain the locators to locate the elements/objects on the screen that your automation tests will interact with.

Tests

Here is an example test file:

import XCTestclass LoginUITest: TestBase {
private var hasRunAtLeastOnce = false
public
lazy var login = Login()
override func setUpWithError() throws {
try super.setUpWithError()
if hasRunAtLeastOnce == false {
hasRunAtLeastOnce = true
XCUIApplication().launch()
self.login
.navigateToLogin()
}
}
override func tearDown() {
super.tearDown()
}
func testLoginButtonDisabledWhenUserNameEmpty() {
addTeardownBlock {
self.login
.clearPasswordField()
}
login
.enterPasscode(“123”)
.verifyLoginButtonIsDisabled()
}
func testLoginButtonDisabledWhenPasswordEmpty() {
addTeardownBlock {
self.login
.clearUserNameField()
}
login
.enterUsername(“sally”)
.verifyLoginButtonIsDisabled()
}
func testLoginButtonDisabledWhenBothFieldsEmpty()
addTeardownBlock {
self.login
.clearUserNameField()
.clearPasswordField()
}
login
.verifyLoginButtonIsDisabled()
}
func testLoginButtonEnabledWhenBothFieldsFilled() {
addTeardownBlock {
self.login
.clearUserNameField()
.clearPasswordField()
}
login
.enterLoginInfo(user: TestPatients.users[5])
.dismissKeyboard()
.verifyLoginButtonIsEnabled()
}
}
  • Each test case must start with “test”
  • The setup function runs before every test. In order to avoid launching the test for every test case, we use the workaround by setting a boolean. We do this due to the time of our application launch + login time. Even with using stub data, this would add several seconds onto each different test case. To reduce the overall test run time, we run all test scenarios for a particular screen and use tearDown blocks (when needed) to reset each test back to where the setUp ends.
  • login is the screen/page object — an instance of the “function” file which will be shown in more detail next.
  • Returning “self” from functions allows you to chain function calls. You will also see this in the function file example.

Functions

Here is an example functions file setup:

import Foundation
import XCTest
class Login {
private lazy var alertHelper = AlertHelper()
private lazy var dashboard = Dashboard()
private lazy var more = More()
private lazy var onboarding = Onboarding()
private lazy var tabBar = TabBar()
private lazy var termsAndConditions = TermsAndConditions()
@discardableResult
func
clearUserNameField() -> Self {
clearTextField(textField: LoginScreen.usernameTextField.element)
return self
}
@discardableResult
func
dismissKeyboard() -> Self
LoginScreen.loginLabel.element.tap()
return self
}
@discardableResult
func
enterLoginInfo(user: Patient) -> Self {
waitForLoginButtonToExist()
LoginScreen.usernameTextField.element.tap()
LoginScreen.usernameTextField.element.typeText(user.username)
waitForPasswordFieldToExist()
LoginScreen.passwordTextField.element.tap()
LoginScreen.passwordTextField.element.typeText(user.password)
return self
}
@discardableResult
func
verifyLoginButtonIsDisabled() -> Self
XCTAssertFalse(LoginScreen.loginButton.element.isEnabled)
return self
}
@discardableResult
func
waitForLoginButtonToExist() -> Self
WaitHelper.waitFor(existence: true,
element: LoginScreen.loginButton.element,
failureLog: "Login Screen login button does not exist.")
return self
}

... additional functions ...
  • These function files contain the functions that are called by the tests to interact with the application.
  • These functions use the element locators maintained in the Element files (shown next).
  • “Helper” function files may also be created that contain functions for functionality common to multiple screens, such as waiting on elements or interacting with alert dialogs. Shown here is a call to WaitHelper which is a Helper File.

Elements

The locators can be organized using swift enumerations.

import Foundation
import XCTest
enum LoginScreen: String {
case cancelButton = "Cancel"
case hideButton = "Hide"
case loginButton = "log_in_button"
case loginLabel = "Username"
case passcodeErrorAlert = "UIAlert"
case passwordTextField = "password"
case showButton = "Show"
case usernameTextField = "username"
var element: XCUIElement {
switch self {
case .loginButton, .showButton, .hideButton, .cancelButton:
return XCUIApplication().buttons[self.rawValue]
case .usernameTextField:
return XCUIApplication().textFields[self.rawValue]
case .passwordTextField:
return XCUIApplication().secureTextFields[self.rawValue]
case .loginLabel:
return XCUIApplication().staticTexts[self.rawValue]
case .passcodeErrorAlert:
return XCUIApplication().navigationBars[self.rawValue]
}
}
}
  • Using Identifiers, which is a recommended practice anyways, works well for this setup.
  • This approach is unable to take parameters so this didn’t always work for all locators for us. For these instances, we create Element Functions section within the Functions files as needed. Example:
// MARK: - Element Functionsfunc medCell(medCell: Int) -> XCUIElement {
return MedicationsScreen.medsListTable.element.cells.element(boundBy: medCell)
}

--

--