Introducing the Sample Project

Foundational Setup: Preparing Your SwiftUI Project

Before diving into the three-phase workflow, a proper project setup is essential. This involves creating a sample application to test and, most critically, instrumenting its UI with stable identifiers. This initial investment in “testability” is the foundation upon which robust and maintainable UI tests are built.

Building the Sample App: “TaskMaster”

To demonstrate the principles of the Record → Replay → Review workflow, this guide will use a simple task management application named “TaskMaster.” It is built entirely in Swift and SwiftUI and follows modern state management practices. In can find the TaskMaster project in the Git repo for this lesson.

The application consists of two main views:

  • TaskListView: Displays a list of tasks. Users can tap a button to navigate to the detail view to add a new task. They can also swipe on a task to mark it as complete.
  • TaskDetailView: A form for adding a new task or editing an existing one.

State is managed by a simple @Observable class called TaskManager, ensuring that UI updates are driven by a single source of truth, a modern pattern in SwiftUI applications that is important to test.  

Adding the UI Testing Target

With the application code in place, the next step is to add a dedicated target for UI tests to the Xcode project.

In Xcode, navigate to File > New > Target….

Xcode Menu File-New-Target
Xcode Menu File-New-Target

In the sheet that appears, scroll down to the bottom and select the UI Testing Bundle template and click Next.

Target selection dialog
Target selection dialog

Keep the default product name (e.g., TaskMasterTests) and ensure the “Target to be Tested” is set to your main application target, TaskMaster.

Target options dialog
Target options dialog

Click Finish.

Xcode will create a new group and file, TaskMasterTests.swift, containing boilerplate test code. This file includes two important methods:  

setUpWithError() and tearDownWithError(). The setUpWithError() method is executed before each test function in the class, making it the ideal place for setup code that ensures each test runs in a clean, isolated state.  

The Cornerstone of Stability: Accessibility Identifiers

The single most important practice for creating stable, maintainable UI tests is the consistent use of accessibility identifiers. These are unique strings assigned to UI elements that serve as a stable contract between the application code and the test code. Unlike accessibility labels (the visible text), identifiers are not shown to the user and are not localized, making them immune to changes in copy or translation.  

The TaskMaster code provided in the sample code has already been instrumented with these identifiers using the .accessibilityIdentifier("...") view modifier. For example, the “Add Task” button in TaskListView is defined as:

Button(action: {
  showingAddTaskView = true
}) {
  Image(systemName: "plus")
}
.accessibilityIdentifier("addTaskButton")

A test can now reliably locate this button using app.buttons regardless of its appearance, language, or position on the screen. This contrasts sharply with a fragile, label-based query like app.buttons["Add"], which would break if the button’s text were changed.

Making the addition of these identifiers a foundational step in the development process fundamentally changes the nature of testing. It ceases to be an activity performed after the UI is complete and becomes an integral part of building the UI itself. This encourages a “testability-first” mindset, where developers consider how a component will be validated as they construct it. This practice not only leads to better tests but also results in higher-quality application code that is more modular, accessible, and easier to maintain over time.

See forum comments
Download course materials from Github
Previous: Introducing the Three-Phase Paradigm Next: The Record -> Replay -> Review Flow