An introduction to UI Testing on iOS
I’ve always been a big fan of UI testing where appropriate, we can use it to automate the validation of visual components in our applications which allows us to reduce bugs, regressions and confusing behaviour within our applications — all helping us to save time from manually checking the behaviour and display of our view components.
In this article we’re going to take a brief look at the XCTest framework which allows us to create a collection of tests for our iOS projects — this time around we’re going to focus on User Interface tests.
At this point I’m going to presume that you have some form of iOS project setup, at this point it doesn’t really matter how simple or complex it is — just as long as there are some components that we can validate on the screen. Before we get started though, we need to setup our first test class. When you first created your project, Xcode should have generated a test class for your UI tests by default (called YourAppName_iOSUITests) — if it isn’t there for some reason, you’ll need to create one. It will need to look something like this:
import XCTestclass SampleApp_iOSUITests: XCTestCase {}
You’ll notice here that our class extends the XCTestCase class — this is the foundation for defining a class that defines a collection of tests. It’s important to note that the test classes you create should be focused — if we aim to group similar responsibilities per test class then we can ensure that these classes will remain fine-grained and easy to maintain. With this in mind, naming your test classes per-responsibility will not only help to enforce this, but also help when navigating your project.
Next we need to define a global reference to our Application instance that will be used during testing — here we define this field as an XCUIApplication instance. This global instance allows us to perform interactions with our test application instance from each test — this allows us to launch / terminate / set state as we require.
var app: XCUIApplication!
Now that we’ve defined this, we’re going to override the XCTestCase setUp function so that we can initialise this instance and configure any other parts that are required for our tests.
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments.append("--uitesting”)
}
Let’s take a look at what each part here is doing:
- We begin by setting the value for the continueAfterFailure field in the XCTestCase class. This defines that if one test fails then we should not continue to run the rest — this is good to set as it saves us time running our tests when the test state is not satisfied.
- Next we initialise our XCUIApplication instance. The setup function is called before each test, so we initialise a new application instance each time around.
- Finally we append an argument to our test command — this specific command signifies that our application is running UI tests, so it should reset its state so that we can reduce any flakiness that may occur when running our tests.
Now that we’ve setup our UI tests, let’s take a look at the kind of tests that we could begin with writing for our screen if choice. Because it’s good practice to test a single piece of functionality per test function, we’re going to write a test for each of the expected cases in a simple authentication screen that I’ve created in my project.
We’re going to start by testing that the Sign Up button is displayed on the screen when the authentication screen is launched.
func testSignUpButtonDisplays() {
app.launch()
XCTAssertTrue(app.buttons[“Sign up”].exists)
}
We begin here by invoking the launch() function on our XCUIApplication instance — calling this will launch out application allowing us to perform checks on and interact with the content shown on screen.
Next we use the XCTAssertTrue() function to check the state of some content, which in this case is whether a button with the text “Sign up” exists. Note: It would be good practice here to reference the string resource that is being referenced within your code. I have simply written the content here for readability. But what is happening here exactly?
- We begin by referencing the buttons field from our app instance. This essentially allows us to perform a query using the XCUIElementQuery class, using the provided argument.
- For this query we provide the “Sign up” string. This tells our query to return us instances of UI components that contain a child with the given string.
- We then use the exists function to determine whether or not there is an element that has been returned by our query.
The XCTAssertTrue call that we make requires for the result of our query to be true. If our exists call is satisfied and the given element exists then this test will succeed. The XCTAssertTrue function also takes a second argument in the form of an NSString which can be used to provide a message for when the test is not satisfied, this may be useful for debugging purposes for when tests do fail.
Now that we know how to check if there are elements present on screen, in some situations we may wish to interact with view components. For the example here, on my authentication screen when you click “Sign up” you are taken to the “Sign up” screen. We’re going to write a simple test to check that when we tap on our “Sign up” button we are no longer on the initial sign-up/sign-in screen.
func testSignUpButtonShowsSignUpUi() {
app.launch()
app.buttons[“Sign up”].tap()
XCTAssertFalse(app.buttons[“Sign in”].exists)
}
Again, we launch our XCUIApplication instance using the launch() function. We are no ready to perform interactions on our UI — at this point we again query the UI components on our screen to find the corresponding component showing “Sign up”, and then use the tap() function to perform a single tap on this component.
At this point, if my application is behaving in the expected way then it will be in the “Sign up” state. Because of this, the “Sign in” button will no longer be visible, so now we use the XCTAssertFalse function to test that our view component (the button showing the “Sign in” text) evaluates to not existing on the screen.
As a last example let’s take a quick look at how we might assert the state of our UI when an alert dialog is shown on screen. When the user is on the sign-up OR sign-in authentication screen, attempting to authenticate without a valid email address will show an alert prompt to let them know of the error here.
func testInvalidEmailMessageDisplays() {
app.launch()
app.buttons[“Sign up”].tap()
app.buttons[“Authenticate”].tap()
XCTAssertTrue(app.alerts.element.staticTexts[
“Please enter a valid email address”].exists)
}
We begin by performing the initial tap() on our “Sign up” button to take us to the authentication screen. Now, without entering any text in the email input field we immediately perform a tap() on the “Authenticate” button. At this point we are expected to be shown an alert dialog with the corresponding message.
To check that this is the case we are going to use our XCUIApplication instance to query that any of the alerts shown on the screen, contain an element which has a staticTexts property of our expected error message. This works exactly the same way as how we have previously checked the state of buttons on the screen, we are just using different query accessors to perform our checks
This has only been a brief look into getting started with UI testing on iOS, but I hope it has given an insight into how little effort it takes to get these test setup in our projects. If you want to learn more about testing on iOS then you can dig more into the documentation for the XCTest framework to discover how you can test the different parts of your application.
If you have any questions about this article or the thoughts I shared, please feel free to reach out! 🙂