Unlocking the Power of UI Testing in iOS

Asilbek Djamaldinov
10 min readOct 9, 2023

--

Here are my articles of the Testing Flow in iOS :

  1. Testing in iOS: From Zero to Hero!
  2. Unlocking the Power of UI Testing in iOS Development (current)
  3. Why Mocking Matters in iOS Unit Testing
  4. iOS Dev Must-Have: Snapshot Testing
  5. Why to Unit Test in Async Mode

In our previous article, we delved into the world of iOS testing. We learned about different types of tests, including unit tests and UI tests, and explored how to incorporate them into your app using Xcode. We even got our hands dirty by writing some basic unit tests. Now, let’s continue our journey by focusing on UI tests and understanding what they are all about.

Why to use UI Tests?

UI tests, short for User Interface tests, are like the inspectors of your app’s graphical user interface (GUI). They are designed to examine how your app’s interface behaves and ensure it functions as expected when users interact with it. UI tests simulate real user interactions and help you spot any hiccups or glitches in your app’s visual elements and user experience.

Navigating the UI Testing Waters in MyOceanTest

In our Xcode project, which we used in last article, we’ll be working with a special directory called the “MyOceanTestsUITests”. This directory is like a treasure trove where we store all our UI tests. It’s the place where we create and manage our UI test files.

If you’re new to iOS testing and haven’t had the chance to peruse my previous article, I strongly encourage you to do so. It’ll provide you with valuable insights and background information that will enhance your understanding of the testing in the article

As you can see in the screenshot, we have a dedicated folder named “MyOceanTestsUITests” within our “MyOceanTests” project. This is where we’ll be adding our UI test files.

Think of this folder as your underwater treasure chest, where each UI test you write is like a precious gem, helping to keep your app shipshape and user-friendly.

Exploring the Depths of Default UI Testing Files in Xcode

Earlier, we talked about the basic files you get when you create unit tests in Xcode. Now, when it comes to UI tests, it’s pretty similar, but there are a few differences. Let’s focus on those differences.

import XCTest

final class MyOceanTestsUITests: XCTestCase {

override func setUp() {
super.setUp() {
// 1
continueAfterFailure = false
}

override func tearDown() {
super.tearDown()
}

func testExample() throws {
// UI tests must launch the application that they test.
// 2
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}

// 3
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
  1. The setUp() and tearDown() functions in UI tests work just like they did in unit tests. They run before and after each test to get things ready and clean up afterward.
    But there’s something interesting in the code: **continueAfterFailure = false.** What this does is, it tells the test to stop running if it encounters a problem or failure. In simpler terms, it's like saying, “Hey, if something goes wrong, don't keep going, just pause and let me fix it”. If just one test doesn't work as expected, the entire testing process will come to a halt and make it very clear that something went wrong.
    If you set it to true, it will keep going and finish running all the tests, even if one of them fails. It won't stop and try to cover all ui tests.
    I like to tackle problems right away, so most of the time, I set it to false because it allows me to address issue immediately.
  2. We have a function named func testExample() throws. Inside the function we create a special thing called appusing XCUIApplication(). This app represents our app (the whole app) that we want to test.
    Then, app.launch() make our app launch, just like how you'd start an app on your phone or computer. This function is like turning on our app and getting it ready for testing. We're preparing to interact with it and check if everything works as it should.
  3. This code is all about measuring how quickly our app launches, like how fast it starts when you open it.
    Here’s what’s happening step by step:
    1. measure(metrics: [XCTApplicationLaunchMetric()]) { ... }: This part sets up a measurement. We're telling Xcode, "Hey, we want to measure something." In this case, we're measuring how long it takes for our app to launch.
    2. Inside the { ... } part: XCUIApplication().launch(): This line starts our app. It's like pressing the "Start" button on your app. We're making the app begin running.

So, when we run this code, it will keep track of how many seconds it takes for our app to start, and it will give us that information. This is important because fast app launches make for a smoother and more enjoyable user experience.

We’ll explore this topic more thoroughly in our next article when we dive deeper into UI testing.

Get Ready to Dive into UI Testing Sea

Enough! Theory is great, but nothing beats getting your hands dirty with some real code. Let’s dive in and start writing some UI tests to see how it all comes together in practice. It’s where the real fun begins! 🚀

Certainly, explaining UI testing is simpler in video, but I’ll do my best to make it as clear as possible using text.

Uncovering Hidden Treasure

Let’s see what we’ll be testing today. 🥳

I’m not just any designer; I’m a professional designer, believe me! 😉

We’ve got a handful of things to test here:

  1. We’ll make sure the “Registration” text is present.
  2. Then, we’ll check if the text field works and opens the soft keyboard.
  3. We’ll examine a button that shouldn’t function when the username field is empty but should navigate when there’s text.
  4. And, of course, we’ll check the back button.

I think that’s a good amount for our initial testing. Excited? Me too.

Last thing before we actually start.

There’s this amazing feature that makes our work way easier. It lets us click on things in our app, and it does the code-writing part for us (thanks, Apple!). To use it, open your UITest file, move your mouse cursor to the line where you want to write your test, and find the red circle button with ‘Record UI Test’ written on it in the bottom-left corner.

When you click that button, your app will start running, and you can interact with it by clicking on different items. As you do, it records the actions you take, creating a sequence of steps. Later, you can use this recorded sequence to create your UI test. It’s like building a path of actions that the test will follow.

After clicking on the “Registration” text multiple times, you’ll notice that inside the function, something like this appears:

func test_deepOceanView_textRegistration_isPresent() {  
let registrationStaticText = XCUIApplication().staticTexts["Registration"]
registrationStaticText.tap()
registrationStaticText.tap()
registrationStaticText.tap()
}

In line let registrationStaticText = … Xcode creates a reference to the UI element with the label “Registration.” It’s like pointing to that specific text on the screen. Next, we’re tapping on the “Registration” text three times in a row and Xcode creates a record. Each tap simulates a user interaction with that text.

By using this approach, you can mimic any actions or steps a user might take within your app’s logic.

Don’t Forget Your Testing Toolkit

Before we jump into the testing, let’s check our gear:

  1. Xcode: Our virtual submarine for exploring the code sea.
  2. XCTest: Our trusty harpoon for tagging and capturing bugs.
  3. Record UI Test Button: This nifty button helps us record our testing actions.
  4. Coffee: Our essential underwater oxygen supply (just kidding, it’s for staying awake during late-night bug hunts). 😁

A Tiny Step for Mankind, But a Giant Leap in UITest Learning!

Great news! We’ve already talked about the “given — when — then” pattern for testing in iOS, and the good news is that we’ll keep using this pattern in UI tests as well. It’s a consistent and effective way to structure our tests.

In the test mentioned above, we have three actions. First, in the “given” part, we create a property called registrationStaticText. Then, we perform the .tap() action, but we'll disregard it because we used it to reference the text itself, not for testing the assertion of its presence. Finally, in the "then" part, we perform the assertion itself.

When it’s all said and done, our test will have the following structure:

func test_deepOceanView_textRegistration_isPresent() {
// given
let app = XCUIApplication()
app.launch()
let registrationStaticText = app.staticTexts["Registration"]

// then
XCTAssertTrue(registrationStaticText.exists, "Text exists")
}

You see, .exists checks if an element is on the screen. It gives us a simple "true" or "false" answer, and we can use XCTAssertTrue to verify whether it's indeed true or not.

Click the diamond icon next to your func test_... to run your test and see if it works. (I'll let you in on a secret — it will work!)

Let’s move on to the second test. We’ll name it test_deepOceanView_usernameTextField_didCallKeyboard. Once again, run the magical red button, click on the text field, and let Xcode do the work of generating the code to start the test. Here’s the final version of the test with the assertion:

func test_deepOceanView_usernameTextField_didCallKeyboard() {
// given
let app = XCUIApplication()
app.launch()
let textField = app.textFields["Username"]

// Make sure the keyboard is not initially displayed
XCTAssertFalse(app.keyboards.element.exists, "Keyboard is not initially visible")

// when
textField.tap()

// then
// Now, check if the keyboard is displayed
XCTAssertTrue(app.keyboards.element.exists, "Keyboard is visible after tapping the text field")
}

This test first verifies that the text field exists, then checks if the keyboard is initially hidden, taps on the text field to trigger the keyboard, and finally confirms that the keyboard is visible after tapping.

Not too challenging, is it?

Let’s dive into the third test case. We’ll be testing two scenarios:

  1. We’ll check a button that shouldn’t do anything when the username field is empty (no navigation).
  2. We’ll verify that it does navigate when there’s text in the username field.

This is interesting because in my code, I’ve added a condition if !username.isEmpty { ... } to prevent navigation from working when the username is empty. So, it should be an interesting test to run.

We will start from the second test case. Let’s give your new test a name: test_deepOceanView_signupButton_didMakeNavigation. Once again, follow the same steps as before: run the magical red button, click on the text field, and tap on the keyboard buttons to interact with the UI. Let Xcode generate the code to initiate this test.

func test_shallowOceanView_signupButton_didMakeNavigation() {
// given
let app = XCUIApplication()
app.launch()
let textField = app.textFields["Username"]
textField.tap()

// when
let AKey = app.keys["A"]
AKey.tap()

let aKey = app.keys["a"]
aKey.tap()
aKey.tap()

app.buttons["Sign Up"].tap()

// then
let helloWorldStaticText = app.staticTexts["Hello world"]
XCTAssertTrue(helloWorldStaticText.exists, "Hello world exists")
}

We simulate typing “Aaa” into the text field by tapping the appropriate keyboard keys. After that, we tap the “Sign Up” button, triggering a navigation action. Then we verify that the “Hello world” static text exists on the screen after the navigation. This is our assertion, confirming that the navigation worked as expected.

For the first case, we’ll follow the same steps, except this time, we won’t tap any keyboard buttons. Instead, we’ll directly tap the “Sign Up” button. In this scenario, navigation should not work, and we shouldn’t see the “Hello world” text. That’s why we switch our assertion from confirming test case is true to checking that it’s false.

func test_shallowOceanView_signupButton_didNotMakeNavigation() {
// given
let app = XCUIApplication()
app.launch()
let textField = app.textFields["Username"]
textField.tap()

// when
app.buttons["Sign Up"].tap()

// then
let helloWorldStaticText = app.staticTexts["Hello world"]
XCTAssertFalse(helloWorldStaticText.exists, "Hello world exists")
}

The final test case in this article is checking whether the back button functions correctly. We’ll name it test_shallowOceanView_backButton_didNavigateBack(). The process of generating code will be the same as in our previous test cases. We’ll tap the back button to verify if it works as expected.

func test_shallowOceanView_backButton_didNavigateBack() {
// given
let app = XCUIApplication()
app.launch()
let textField = app.textFields["Username"]
textField.tap()

let AKey = app.keys["A"]
AKey.tap()

let aKey = app.keys["a"]
aKey.tap()
aKey.tap()

app.buttons["Sign Up"].tap()

let helloWorldStaticText = app.staticTexts["Hello world"]
XCTAssertTrue(helloWorldStaticText.exists, "Hello world exists")

// when
app.navigationBars["Shallow Ocean View"].buttons["Back"].tap()

// then
let registrationStaticText = app.staticTexts["Registration"]
XCTAssertTrue(registrationStaticText.exists, "Text exists")
}

After pressing the back button on the ShallowOceanView, we verified our location by checking if the “Registration” text is present. This text should be on the main page where we expect to navigate back to.

Phew! That was quite the adventure, but we nailed it! 😅

Tests’ code optimisation

We have an opportunity to streamline our code significantly, but we’ll save most of those optimisations for our next UITesting article. For now, let’s simplify things by moving the let app = XCUIApplication() and app.launch()lines from each method into a class variable and launch the app in the setUp() method.

final class MyOceanTestsUITests: XCTestCase {
let app = XCUIApplication()

override func setUp() {
app.launch()
continueAfterFailure = false
}

...
}

Make sure to tidy up your test methods. Run the tests in your UITest file and take a look at the outcome. You’ve done fantastic work!

Conclusion

In conclusion, UI testing is a crucial aspect of iOS app development, allowing us to ensure that our app’s user interface functions as expected. By creating UI tests in Xcode and following the “given — when — then” pattern, we can thoroughly examine different aspects of our app’s user experience.

This article provided an overview of UI testing in iOS development, demonstrated how to set up UI tests in Xcode, and walked through several practical test cases. While there’s more to explore in UI testing, this article laid the foundation for conducting effective tests and improving the quality of iOS apps. So, get ready to dive deeper into the exciting world of UI testing in your iOS development journey! 🚀

Find it a good read?

Recommend this post by clicking the 👏 button so other people can see it and let’s connect on LinkedIn asilbekdjamaldinov

--

--