Hello! Welcome back to our course.
In our last lesson, we laid the essential groundwork for UI testing by learning how to make UI elements discoverable through accessibilityIdentifier. We saw how a stable, developer-facing identifier is the key to building robust and maintainable tests.
Now, it's time to put those identifiers to work. This lesson moves from preparation to action. Our goal is to write an XCUI Test to simulate a user flow, including taps, swipes, and text input. We will automate the actions a real user would take, verifying that our app behaves as expected from end to end. This is where we ensure all the individual components of our app come together to create a seamless user experience.
The Anatomy of an XCUI Test
Before we write our first test, let's understand the basic structure provided by Xcode's UI testing framework, XCUITest. A UI test is a class that inherits from XCTestCase and typically contains four key parts:
XCUIApplication: A proxy object that represents your running application. All interactions start here.setUpWithError(): A method that runs before each test function in the class. This is the perfect place to launch the app (app.launch()) to ensure every test starts from a clean, known state.tearDownWithError(): A method that runs after each test function. You can use this for cleanup, like terminating the app (app.terminate()).test...()methods: Functions whose names begin with the word "test". Xcode automatically discovers and runs these as individual test cases. Each one should verify a specific piece of functionality or user flow.
To solidify these foundational concepts, please read this excellent article from Nathan Krishnan. It provides a clear, detailed walkthrough of the structure of a UI test class.
Read the section 'The UI Test Class', including its subsections: 'Setup', 'XCUIApplication', 'XCUIDevice', 'Teardown', and 'Test Methods'. Focus on understanding the role of each part in the test lifecycle.
Writing Your First Test: The Record and Refine Workflow
The fastest way to get started with a UI test is to let Xcode do the initial work for you. Xcode includes a "Record" feature that watches you interact with your app in the simulator and translates your actions into test code.
However, a professional developer knows that the recorded code is just a starting point. It's often messy and relies on fragile locators like visible text. Our workflow will be to Record, then Refine.
Let's watch a demonstration of this workflow. The video will show you how to:
- Use the record button to generate test code for a sign-up flow.
- Clean up the generated code.
- Use the
accessibilityIdentifiers we learned about in the last lesson to make the test robust. - Add an
XCTAssertto verify that the flow was successful.
UI Testing a SwiftUI application in Xcode | Advanced Learning #18
This video from the Swiftful Thinking channel provides a complete walkthrough of recording a user flow, refining the code, and adding an assertion.
Please watch the following segments: Recording the Flow (26:26 - 28:18): See how to start the recorder and perform actions like tapping a text field and a button. Refining and Asserting (28:18 - 36:29): Observe how the messy recorded code is cleaned up and structured, and how an XCTAssert is used to check that the user successfully navigated to the 'Welcome' screen. Using Accessibility Identifiers (36:29 - 40:17): This is a critical step that connects to our last lesson. Pay close attention to how the test is modified to use stable identifiers instead of breakable UI labels.
Simulating Core User Interactions
The recording feature showed us the basics, but let's break down the main actions you'll use to build any user flow. At the core, you'll query for an element and then call an action method on it.
Taps and Text Input
Tapping buttons and typing in text fields are the most common interactions.
- Tapping: You use the
.tap()method on an element.let signUpButton = app.buttons["SignUpButtonAccessibilityID"] signUpButton.tap() - Typing Text: You can use
.typeText(_:)to enter a string into a text field or text view. The test will automatically tap the field to focus it first.let emailField = app.textFields["EmailFieldAccessibilityID"] emailField.typeText("user@example.com")
For a detailed look at simulating text input and other keyboard actions, this resource is very helpful.
Beginners' Guide to UI Testing for iOS in Xcode
This article from TestDevLab provides concise code examples for typing text and interacting with the keyboard.
Read the sections 'Typing and keyboard actions' and 'Managing text fields and validating input'. These provide clear examples of how to type, delete, and validate text.
Swipes
To test scrolling through lists, navigating carousels, or dismissing views, you'll need to simulate swipes. XCUIElement provides simple methods for this.
.swipeUp().swipeDown().swipeLeft().swipeRight()
You can call these on a specific element (like a ScrollView) or on the app itself to simulate a general swipe gesture.
let scrollView = app.scrollViews["ProductListScrollView"]
// Scroll down to reveal more content
scrollView.swipeUp()
// Go to the next item in an image carousel
let imageCarousel = app.otherElements["ImageCarouselView"]
imageCarousel.swipeLeft()
Handling Asynchronicity: Avoiding Flaky Tests
What happens when an element doesn't appear on screen immediately? This is common with animations or when data is being fetched from a network. A naive test would fail because it would look for the element before it exists.
A common but incorrect solution is to use sleep(). This is bad practice because it either slows down your tests unnecessarily (if the delay is too long) or remains flaky (if the delay is too short).
The professional solution is to use expectations. The waitForExistence(timeout:) method tells the test to wait for a specific element to appear for up to a certain amount of time.
UI Testing a SwiftUI application in Xcode | Advanced Learning #18
Let's return to the Swiftful Thinking video to see a practical example. The test needs to verify an alert appears, but there's a slight delay. This segment shows why sleep() is not ideal and how to correctly use waitForExistence.
Watch from 50:50 to 55:24. The key takeaway is the replacement of a flaky sleep() call with a robust waitForExistence check. This is a crucial technique for professional-grade UI tests.
Tying It All Together: A Multi-Step Flow
Now let's combine everything we've learned—tapping, typing, swiping, and waiting—to test a complete, multi-step user flow. We will simulate a user who:
- Signs in.
- Navigates to a new screen.
- Taps a navigation link to go to a destination.
- Taps the back button to return.
UI Testing a SwiftUI application in Xcode | Advanced Learning #18
This final clip from Swiftful Thinking demonstrates a test for a navigation flow, bringing together element queries, taps, and assertions.
Watch from 56:27 to 1:02:05. Notice how each step of the user's journey is represented by a command in the test, and how assertions are used to confirm that the app is in the correct state after each major action.
As you saw in the video, more complex tests can be built by chaining these simple interactions and assertions together, perfectly mimicking a real user's journey through your application.
Conclusion
You now have the fundamental skills to automate user flows in your iOS apps. You've moved beyond just making elements discoverable to actually interacting with them in a meaningful way to verify your app's behavior.
Key Takeaways:
- UI tests are structured within
XCTestCasesubclasses, usingsetUpWithErrorandtearDownWithErrorfor a consistent test environment. - The Record and Refine workflow is an effective way to start, but you must refine the generated code using
accessibilityIdentifiers to ensure your tests are robust. - You can simulate user actions with methods like
.tap(),.typeText(), and.swipeLeft() / .swipeUp(). - To avoid flaky tests caused by animations or network delays, use
waitForExistence(timeout:)instead ofsleep(). - A complete user flow can be tested by combining a sequence of element queries, actions, and assertions.
Preview of the Next Lesson:
In this lesson, we used simple assertions like .exists to check if an element appeared. But what if we need to verify more specific details? In our next lesson, we will focus on using element queries and assertions to verify UI state during a UI test. We'll learn how to check an element's label, value, count, and other properties to write much more precise and powerful tests.