Wesley de Groot's Blog
A Guide to UI Testing in Swift

Back

User Interface (UI) testing is a crucial aspect of app development, ensuring that your app's interface behaves as expected under various conditions.
This guide will walk you through the basics of UI testing in Swift and provide tips for creating effective tests.

What is UI Testing?

UI testing involves interacting with your app's user interface to verify that it functions correctly.
This includes checking that buttons respond to taps, text fields accept input, and views display the correct content.
UI tests simulate user interactions and can help catch bugs that might not be evident through unit tests alone.

Why UI Testing is Important

  • Catch UI Bugs Early: UI tests can detect issues such as layout problems, missing elements, or incorrect text before users encounter them.
  • Improve User Experience: By ensuring that your app's interface works as intended, you can provide a better experience for your users.
  • Increase Confidence in Code Changes: UI tests can help you verify that new features or changes don't break existing functionality.
  • Automate Testing: UI tests can be automated to run on different devices and configurations, saving time and effort during the testing process.

Key Components of UI Testing

  • XCUIApplication: Represents your app and provides methods to launch, terminate, and interact with it.
  • XCUIElement: Represents a UI element in your app, such as a button, label, or text field. You can query and interact with these elements.
  • XCUIElementQuery: An object that defines the search criteria a test uses to identify UI elements.
  • XCTAssert: Provides various assertion methods to verify that your tests produce the expected results.

Writing Effective UI Tests

  1. Use Accessibility Identifiers: Assign unique accessibility identifiers to your UI elements.
    This makes it easier to locate and interact with elements in your tests.

    button.accessibilityIdentifier = "MyButton"
  2. Launch Arguments and Environment Variables: Use launch arguments and environment variables to configure your app's state before running tests.
    This can help you test different scenarios without modifying your app's code.

    let app = XCUIApplication()
    app.launchArguments.append("--UITestMode")
    app.launchEnvironment["username"] = "testUser"
    app.launch()
  3. Wait for Elements: Use expectations to wait for elements to appear or disappear.
    This ensures that your tests don't fail due to timing issues.

    let label = app.staticTexts["MyLabel"]
    let exists = NSPredicate(format: "exists == true")
    expectation(for: exists, evaluatedWith: label, handler: nil)
    waitForExpectations(timeout: 5, handler: nil)
  4. Test Different Devices and Orientations: Run your tests on different devices and orientations to ensure your app works well across various screen sizes and configurations.

Running and Debugging UI Tests

  • Run Tests: You can run your UI tests by selecting the test target and clicking the "Run" button in Xcode.

  • Debug Tests: If a test fails, Xcode provides detailed logs and screenshots to help you diagnose the issue. Use breakpoints and the debugger to step through your test code and inspect the app's state.

UI Test functions

Basic Functionality

Testing if an element exists

XCTAssert(app.staticTexts["Welcome"].exists)

Waiting for an element to appear

"Waiting" is now built into XCTest.

let throwPokeball = app.staticTexts["The pokemon escaped!"]
XCTAssertFalse(throwPokeball.exists)

app.buttons["Catch!"].tap()
XCTAssert(throwPokeball.waitForExistence(timeout: 5))

Interacting with System Controls

Tapping buttons

Identify buttons by their accessibility label.

app.buttons["Add"].tap()

Typing text

First make sure the text field has focus by tapping on it.

let textField = app.textFields["Username"]
textField.tap()
textField.typeText("Wesley de Groot")

Dismissing alerts

app.alerts["Alert Title"].buttons["Button Title"].tap()

Dismissing action sheets

app.sheets["Sheet Title"].buttons["Button Title"].tap()

Handling system alerts

Present a location services authorization dialog to the user and dismiss it with the following code.

Before presenting the alert add a UI Interruption Handler. When this fires, dismiss with the "Allow" button.

addUIInterruptionMonitor(withDescription: "Location Services") { (alert) -> Bool in
  alert.buttons["Allow"].tap()
  return true
}

app.buttons["Request Location"].tap()
app.tap() // need to interact with the app again for the handler to fire

Sliding sliders

This will slide the value of the slider to 70%.

app.sliders.element.adjust(toNormalizedSliderPosition: 0.7)

Interacting with pickers

A picker:

app.pickerWheels.element.adjust(toPickerWheelValue: "Picker Wheel Item Title")

Tapping links in web views

app.links["Share this"].tap()

Interactions

Pull to refresh

You can simulate a pull-to-refresh action by swiping down on the screen.
(if it is not refreshing, try calling it twice)

app.tables.swipeDown()

Pushing and popping view controllers

Test if a view controller was pushed onto the navigation stack.

app.buttons["More Info"].tap()
XCTAssert(app.navigationBars["Volleyball?"].exists)

Pop a view controller by tapping the back button in the navigation bar and assert that the title in the navigation bar has changed.

app.navigationBars.buttons.elementBoundByIndex(0).tap()
XCTAssert(app.navigationBars["Volley"].exists)

Screenshots

Taking a screenshot

// Take a screenshot
let screenshot = XCUIScreen.main.screenshot()

// Attach the screenshot to the test report
let fullScreenshotAttachment = XCTAttachment(screenshot: screenshot)

// Set the attachment's lifetime to keep it around beyond the test run
fullScreenshotAttachment.lifetime = .keepAlways

// Add the attachment to the test report
add(fullScreenshotAttachment)

Setting Up UI Testing in Xcode

  1. Create a UI Test Target: When you create a new project in Xcode, you can add a UI test target by selecting the "Include UI Tests" checkbox.
    If you already have a project, you can add a UI test target by going to File > New > Target and selecting "UI Testing Bundle".

  2. Write Your First UI Test: Xcode generates a template UI test file for you. Open this file and you'll see a basic test method.
    You can start writing your own tests by using the XCUIApplication class to interact with your app.

    import XCTest
    
    class MyAppUITests: XCTestCase {
    
        func testExample() {
            let app = XCUIApplication()
            app.launch()
    
            // Interact with the UI
            let button = app.buttons["MyButton"]
            XCTAssertTrue(button.exists)
            button.tap()
    
            let label = app.staticTexts["MyLabel"]
            XCTAssertEqual(label.label, "Expected Text")
        }
    }

Frameworks for UI Testing in Swift.

XCUITestHelper Framework benefits

XCUITestHelper by Me I've written this framework because I was missing some (for me essential) features.

XCAppTest Framework benefits

XCAppTest by Łukasz Rutkowski adds a ton of extra functions for testing.
I want to highlight those in particular:

Bonus: Taking screenshots for the App Store

Taking screenshots for the App Store can be a tedious task.
It is possible to automate this with UI tests.
I refer to a blog post of my friend Daniel for this.

Daniel uses this function to name the screenshots:

func takeScreenshot(named name: String) {
    // Take the screenshot
    let fullScreenshot = XCUIScreen.main.screenshot()

    // Create a new attachment to save our screenshot
    // and give it a name consisting of the "named"
    // parameter and the device name, so we can find
    // it later.
    let screenshotAttachment = XCTAttachment(
        uniformTypeIdentifier: "public.png", 
        name: "Screenshot-\(UIDevice.current.name)-\(name).png",
        payload: fullScreenshot.pngRepresentation, 
        userInfo: nil)

    // Usually Xcode will delete attachments after 
    // the test has run; we don't want that!
    screenshotAttachment.lifetime = .keepAlways

    // Add the attachment to the test log, 
    // so we can retrieve it later
    add(screenshotAttachment)
}

With a bash script, you can run the tests on different simulators, languages, and appearances.
This script will create a folder structure with the screenshots for each combination.

#!/bin/bash

# The Xcode project to create screenshots for
projectName="./Libi.xcodeproj"

# The scheme to run tests for
schemeName="Libi"

# All the simulators we want to screenshot
# Copy/Paste new names from Xcode's
# "Devices and Simulators" window
# or from `xcrun simctl list`.
simulators=(
    "iPhone 8"
    "iPhone 11 Pro"
    "iPhone 11 Pro Max"
    "iPad Pro (12.9-inch) (3rd generation)"
    "iPad Pro (9.7-inch)"
)

# All the languages we want to screenshot (ISO 3166-1 codes)
languages=(
    "en"
    "de"
    "fr"
)

# All the appearances we want to screenshot
# (options are "light" and "dark")
appearances=(
    "light"
    "dark"
)

# Save final screenshots into this folder (it will be created)
targetFolder="/Users/breakthesystem/Desktop/LibiScreenshots"

## No need to edit anything beyond this point
for simulator in "${simulators[@]}"
do
    for language in "${languages[@]}"
    do
        for appearance in "${appearances[@]}"
        do
            rm -rf /tmp/LibiDerivedData/Logs/Test
            echo "📲  Building and Running for $simulator in $language"

            # Boot up the new simulator and set it to 
            # the correct appearance
            xcrun simctl boot "$simulator"
            xcrun simctl ui "$simulator" appearance $appearance

            # Build and Test
            xcodebuild \
                -testLanguage $language \
                -scheme $schemeName \
                -project $projectName \
                -derivedDataPath '/tmp/LibiDerivedData/' \
                -destination "platform=iOS Simulator,name=$simulator" \
                build test
            echo "🖼  Collecting Results..."
            mkdir -p "$targetFolder/$simulator/$language/$appearance"
            find /tmp/LibiDerivedData/Logs/Test -maxdepth 1 -type d -exec xcparse screenshots {} "$targetFolder/$simulator/$language/$appearance" \;
        done
    done

    echo "✅  Done"
done

Conclusion

UI testing is an essential part of ensuring your app's quality and reliability.
By writing effective UI tests in Swift, you can catch bugs early, improve your app's user experience, and gain confidence in your code.
With the XCTest framework and the tips provided in this guide, you'll be well on your way to mastering UI testing in Swift.

Resources:

Thanks:

Nb: This post is using different headings and is written in a different style than the other posts.
Let me know if you like it or not.

Read more

Share


Share Mastodon Twitter LinkedIn Facebook
x-twitter mastodon github linkedin discord threads instagram whatsapp bluesky square-rss sitemap