iOS UI tests from scratch
Sveta Novopoltseva
Posted on May 30, 2023
The work of many QA engineers involves developing test cases and verifying the application following them. The larger the application, the more test cases there are, and the more time regression testing takes. But what if we could entrust at least a portion of regression testing to machines?
This is where application testing with UI tests comes to the rescue.
These tests simulate user interactions and allow for automatic verification of various states of the application.
UI tests can be used to verify navigation between screens, whether a specific element is displayed, whether the desired text appears on a button, and so on.
In this article, I will explain and provide examples of how to get started writing simple Swift tests for automating the testing of iOS applications.
To write tests, you will need a basic knowledge of Swift (including basic syntax and understanding of object-oriented programming) and a Mac computer since the testing environment and simulators are available only on macOS.
Gearing up!
To write tests, we need to install the Xcode application.
Most of you probably already have it installed and are familiar with it, but if not, below is a brief instruction:
To install Xcode, it is recommended to update your macOS to the latest version. The latest versions of Xcode are typically compatible with the latest major releases of macOS.
There are two main ways to install Xcode:
- Downloading from the Mac App Store is the easiest method, but it can make updating Xcode more complicated and time-consuming. It can also be more challenging to maintain two versions of Xcode if needed.
- Downloading from developers.apple.com is the method preferred by most developers because it offers greater flexibility. Visit https://developer.apple.com/download/all/ and download the latest stable version of Xcode. Make sure to select the stable version and not the Beta or Release Candidate versions. Once the download is complete, unpack the downloaded file and move the Xcode application to the Applications folder.
After installation, open Xcode. Upon the first launch, you will be prompted to install additional components. Agree to install them.
After installing the additional components, a project selection window will open. This indicates that Xcode is ready for use.
A lab-rat app
For your first test writing experience, it's best to use a simple application of your own. For example, you can create a simple application after learning the basic Swift skills or work on a pet project.
In this article, I will use my own pet project as an example. It is a very simple application designed for trainers to manage their client database.
In this application, you can:
- Create new clients
- Add funds to a client's balance
- Deduct money from a client's balance for a training session
- View a training log
- Make notes about a client
The application has the following appearance
(Home Screen (Displaying a list of clients and Option to add a new client)
In this article, we won't be covering additional screens for balance top-up, deduction, and viewing transaction history.
Test cases for the first test:
The flow that we will be testing is the creation of a new client. Initially, we will focus on the positive scenario. We want to check the following aspects:
- The client creation screen opens.
- On the screen, you can enter the client's first name, last name, phone number, training cost, select pricing options, and fill in notes.
- After clicking the "Done" button, the client creation screen should close, and a new cell with the entered first name and last name should appear in the client list.
Test cases are always better if written before the actual test so that during test implementation, we can focus solely on writing the code. If we write test cases while developing the tests, the quality of the test cases may suffer, and we may overlook important details.
Creating a target for UI tests in Xcode
To enable writing UI tests, you need to add a target for UI testing to your existing project. Follow these steps:
Open Xcode and your project.
In the top menu, click on "File" -> "New" -> "Target".
In the window that appears, select "UI Testing Bundle" from the available options.
In the target name window, leave everything as is and click "Finish".
After this, a folder named "fizrookUITests" appears in my project, containing two files: "fizrookUITests" and "fizrookUITestsLaunchTests"
Our UI Test target is now set up.
Now let's move on to discussing XCTest.
Figuring out XCTest
Let's navigate to the "fizrookUITests" file. In this file, we have a pre-written test template:
Let's go step by step to understand what is included:
- Import XCTest — is an import statement for the testing framework. We need this import in every test file because we'll be using classes from XCTest in all our tests.
- class fizrookUITests: XCTestCase — This line represents class inheritance from the XCTestCase class. When running tests, XCTest searches for all classes that inherit from XCTestCase and executes the tests within them. Each test we write should inherit from XCTestCase.
- func testExample() throws — This is the declaration of a function that represents one of the tests. When running tests, XCTest looks for all methods starting with "test" and executes each method as a separate test.
- func testLaunchPerformance() throws — This is a written test that measures the launch speed of the application. We don't need this test for our basic tests, so it should be removed..
- We won't delve into the remaining parts for now as they are needed for a deeper dive into XCTest.
The tests from this file can already be executed. Although they don't verify anything, we can run them and see the simulator launching, as well as the test execution data being displayed in the console.
To run the tests, click on the rhomb-shaped icon located to the left of "fizrookUITests".
After clicking the test run button, the simulator will open, and the only test "testExample" will be executed. Passed tests will be marked with green checkmarks, and information about the passed tests will be displayed in the console.
Now we can proceed to writing our own test.
Writing the first test
In our first test, we will go through the client creation flow. We will do this based on the test cases we wrote earlier.
To simplify the process of writing tests, Xcode provides a feature called Test Recorder, which allows you to record actions in the simulator and convert them into code.
To use it, place the cursor in the test method and click on the record button.
Let's start by writing a new method that will contain all the logic related to testing the creation of a new client:
func testCreateNewCustomer() throws {
}
Now place the cursor inside this method and click on the red recording icon.
After clicking the button, the simulator will open, and inside it, we perform the actions to create a client:
During the execution of actions in Xcode, the code for these actions will be recorded, resulting in the following code:
let app = XCUIApplication()
app.navigationBars["Clients"].buttons["Add"].tap()
let tablesQuery = app.tables
let firstNameTextField = tablesQuery.textFields["First name"]
firstNameTextField.tap()
let lastNameTextField = tablesQuery.textFields["Last name"]
lastNameTextField.tap()
let phoneTextField = tablesQuery.textFields["Phone"]
phoneTextField.tap()
let trainingPriceTextField = tablesQuery.textFields["Training Price"]
trainingPriceTextField.tap()
app.tables.staticTexts["Choose"].tap()
app.collectionViews.buttons["Per session"].tap()
let textView = tablesQuery.cells.containing(.staticText, identifier:"Notes").children(matching: .textView).element
textView.tap()
app.navigationBars["New Client"].buttons["Done"].tap()
Here we can see how the following actions were performed step by step:
- We located the navigation bar and clicked on the "Add" button.
- We found the text fields with the labels "First name," "Last name," "Phone," and "Training Price" and tapped on them.
- We clicked on the "Choose" button and selected the "Per session" option.
- We located the text view in the cell labeled "Notes" and navigated to it.
- Once again, we found the navigation bar and clicked on the "Done" button.
Most of the actions from the simulator were recorded, except for keyboard input. For recording text input, there are two options:
- Enter data using the simulator's keyboard.
- Enter data programmatically in the code.
We will now apply the second option. For entering data, each text field has a method called typeText
.
Let's add to our code using the typeText
method in the following way:
func testCreateNewCustomer() throws {
let app = XCUIApplication()
app.navigationBars["Clients"].buttons["Add"].tap()
let tablesQuery = app.tables
let firstNameTextField = tablesQuery.textFields["First name"]
firstNameTextField.tap()
firstNameTextField.typeText("John")
let lastNameTextField = tablesQuery.textFields["Last name"]
lastNameTextField.tap()
firstNameTextField.typeText("Apple")
let phoneTextField = tablesQuery.textFields["Phone"]
phoneTextField.tap()
phoneTextField.typeText("+352911678442")
let trainingPriceTextField = tablesQuery.textFields["Training Price"]
trainingPriceTextField.tap()
trainingPriceTextField.typeText("100")
app.tables.staticTexts["Choose"].tap()
app.collectionViews.buttons["Per session"].tap()
let textView = tablesQuery.cells.containing(.staticText, identifier:"Notes").children(matching: .textView).element
textView.tap()
textView.typeText("Powerful")
app.navigationBars["New Client"].buttons["Done"].tap()
}
Now let's add the following line to the beginning of the test, right after the let app = XCUIApplication()
line:
app.launch()
This is necessary to launch the application at the beginning of the test. Now you can run the test and see what will be executed in the simulator. Click on the rhomb-shaped icon next to our test and look to the simulator:
We can see how miraculously our code executes the entire set of required actions, including text input! This is excellent.
Now, let's recall our initial task: we need to verify that the client is actually created and displayed in the client list. Our test already goes through the entire flow, and now we just need to add a check for the presence of the client in the list.
Test Recorder won't help us with checking the presence of a client in the list, so we need to write the test ourselves.
Since we will be searching for the created client in the client list based on their name and last name, let's first extract the name and last name into separate variables so that we can use them in further checks. Let's modify the existing code as follows:
app.navigationBars["Clients"].buttons["Add"].tap()
let firstName = "John"
let lastName = "Apple"
let tablesQuery = app.tables
let firstNameTextField = tablesQuery.textFields["First name"]
firstNameTextField.tap()
firstNameTextField.typeText(firstName)
let lastNameTextField = tablesQuery.textFields["Last name"]
lastNameTextField.tap()
lastNameTextField.typeText(lastName)
Now, when creating a client, the data from the variables "firstName" and "lastName" will be entered.
Let's move on to checking the presence of the client in the list. Add the following code to the end of the test:
let nameOfNewCustomer = firstName + " " + lastName
let isNewCustomerExists = tablesQuery.staticTexts[nameOfNewCustomer].exists
XCTAssertTrue(isNewCustomerExists)
What we're doing here:
- We create a variable with the final name of the client, which will be "John Apple".
- We retrieve the presence of the element with the static text of the client's name on the screen and store it in a variable.
- We use an XCTest assertion to check if the variable representing the presence of the element is true. If the variable is false, the test will fail.
Now we can run the test and see what happens.
Our test is passing, and the check for the presence of a new customer is being performed.
For verification purposes, we can try to break the test, for example, by removing the space between firstName and lastName in the nameOfNewCustomer variable and verify that in such a case the test will fail.
That way, we have written a test that fully goes through the customer creation flow and verifies the presence of the customer in the customer list after creation.
And so what?
- Working with accessibility identifiers - in our test, we are currently using text-based element references. This means that our test would break if the language or any texts change. To prevent this, we should use accessibility identifiers. You can learn more about them here: https://ayaakl.wordpress.com/2020/04/19/making-your-ios-app-accessible-for-ui-tests-ayas-cookbook-about-ios-accessibility-identifiers/
- Managing different UI elements such as UISlider, TableView, and others https://www.hackingwithswift.com/articles/148/xcode-ui-testing-cheat-sheet
- Using different methods to locate the desired UI elements in your application is crucial. Test Recorder may not always find the most optimal ways to access elements, so it's important to learn and utilize alternative methods for locating interface elements effectively. https://www.browserstack.com/guide/xcuitest-locators-to-find-elements
- Working with asynchronous events in an application, such as network requests, is crucial. Most of our applications utilize network requests, and tests need to be able to wait for their responses. You can find more information on this topic here: https://medium.com/quality-engineering-university/xcuitests-why-how-to-apply-wait-for-element-e42ef300ca93
Automated testing is a great opportunity for a tester's growth. With proper dedication, it allows for easier testing of simple and routine scenarios, freeing up a lot of time for more important and interesting tasks.
Posted on May 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.