A Guide to UI Testing in Swift
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
-
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"
-
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()
-
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)
-
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
-
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 toFile > New > Target
and selecting "UI Testing Bundle". -
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 theXCUIApplication
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.
- XCUIApplication.setLanguage(to:)
This sets the user-interface language of the application which you are running. - XCUIApplication.wait(for:)
This wait for a defined period before continuing - XCUIApplication.navigateBack()
This navigates back (if you are using a NavigationView) - XCUIElementQuery.random
This selects a random element - XCUIElementQuery.lastMatch
This gives the last match of an predicate - XCUIElementQuery.subscript(_:)
This supports that you can uselistOfElements[number]
- XCUIElementQuery.type(in:text:action:)
This types some text in the inputfield and can press continue/enter.
XCAppTest Framework benefits
XCAppTest by Łukasz Rutkowski adds a ton of extra functions for testing.
I want to highlight those in particular:
-
XCUIElement.tapWhenReady(timeout:_:file:line:)
This taps the element when it is ready. -
XCUIElement.tapIfExists(timeout:_:file:line:)
This taps the element if it exists. -
XCUIElement.pressWhenReady(forDuration:timeout:_:file:line:)
This presses the element when it is ready. -
XCTestCase.run(_:function:block:)
This runs a function on the element.
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:
- XCUITestHelper by Wesley de Groot.
- XCAppTest by Łukasz Rutkowski.
- UI Testing Cheat Sheet by Joe Masilotti.
- Blog post: Creating automated Screenshots using XCUITest by Daniel.
Thanks:
- Thanks Łukasz for mentioning XCAppTest!
- Thanks Daniel for your Blog post
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
- A Guide to UI Testing in Swift • 15 minutes reading time.
- Default values for UserDefaults • 2 minutes reading time.
- Snippet: @EnvironmentVariable • 1 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook