UI Tests, Test Plans, Performance Tests and Coverage
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Unit Testing with Swift Testing tested FieldNotes from the inside: the store, the persistence layer and the intents, called directly. This page tests it from the outside. UI tests launch the real app and use it the way a person does; performance tests measure how long it takes; a test plan runs everything in several languages and with runtime checks enabled; code coverage shows what none of the tests reached. Everything targets the 27 releases (iOS 27, iPadOS 27, macOS 27, watchOS 27 and visionOS 27), built with Xcode 27 and Swift 6.4, with the app’s deployment minimum of 26. None of the APIs on this page is new in 27; the Xcode features used (video capture in test plans) arrived in Xcode 26.
UI tests and performance tests are written with XCTest, because Swift Testing doesn’t support UI automation or
performance metrics. XCTest itself (test classes, setUp, the XCTAssert family) is covered in the Swift reference
under Testing and, for Objective-C, under
Testing. The general case for having few end-to-end tests and
many unit tests is made for the web in E2E Testing in Real Browsers; the
reasoning is the same for apps.
Where UI Tests Fit
A UI test is the most realistic test and the most expensive one. It builds and launches the whole app, waits for real animations, and fails for reasons that have nothing to do with the code under test, such as a keyboard that covers a button. FieldNotes therefore tests behaviour at the lowest level that can see it, and keeps UI tests for what only the running app can show: that the screens are wired together, that the app is accessible, and how long it takes to launch.
Figure: FieldNotes’s test pyramid
NoteCaptureUITests, AccessibilityAuditTests,
FieldNotesShortcutsTests, LaunchPerformanceTests
XCTest + XCUIAutomation in FieldNotesUITests"] INT["Integration tests -- some, real frameworks
PersistenceTests: in-memory SwiftData
StoreManagerTests: StoreKitTest
Swift Testing"] UNIT["Unit tests -- many, fast, isolated
NoteStoreTests, NoteSortOrderTests
Swift Testing in FieldNotesTests and FieldNotesKitTests"] UI --> INT --> UNIT classDef ui fill:#fde2e2,stroke:#b42318,color:#1f1f1f classDef int fill:#fff4d6,stroke:#b54708,color:#1f1f1f classDef unit fill:#dcfae6,stroke:#067647,color:#1f1f1f class UI ui class INT int class UNIT unit
The width of each layer is the number of tests: dozens of unit tests run in seconds on every build, a handful of integration tests use real SwiftData and StoreKit configurations, and a few UI tests run before merging and in CI. When a UI test fails, the first question is whether a unit test could have caught the same bug.
UI Automation with XCUIAutomation
UI tests use XCUIAutomation, which XCTest imports automatically. The test runs in its own process and controls
the app through the accessibility system: XCUIApplication launches and terminates the app, queries such as
app.buttons["capture-button"] find elements, and XCUIElement methods such as tap() and typeText(_:) act on
them. The test never calls the app’s code, so it sees exactly what a person or VoiceOver sees.
UI testing isn’t available for apps built with the visionOS SDK; Apple Vision Pro can only run UI tests for compatible iPhone and iPad apps. FieldNotes is a native visionOS app, so its UI tests run on the iPhone, iPad and Mac destinations.
Accessibility identifiers
A query can find an element by its label ("Save"), but labels are localized: the same test would fail in the German configuration, where the button may read "Sichern". An accessibility identifier is a stable, non-localized string that assistive technologies ignore; it exists for tests. FieldNotes sets identifiers on the elements its UI tests touch:
// Excerpts (updated) from NoteList, NoteListToolbar, NoteDraftForm and NoteComposer: identifiers for UI tests
List(visibleNotes) { note in
NoteRow(note: note)
}
.accessibilityIdentifier("note-list")
CaptureButton(action: capture)
.accessibilityIdentifier("capture-button")
TextField("Title", text: $draft.title)
.accessibilityIdentifier("note-title-field")
Button("Save") {
store.add(Note(title: draft.title, body: draft.body, notebookID: notebookID, tags: draft.tags))
dismiss()
}
.disabled(draft.title.isEmpty)
.accessibilityIdentifier("save-note-button")
Identifiers must never replace labels; VoiceOver still needs "Capture" and "Save". The rules for labels are on
Accessibility. On the Mac, the New Note toolbar button carries the same
"capture-button" identifier, so one test works on every destination.
Launching FieldNotes for a test: --uitesting
A UI test must start from a known state. If it ran against the developer’s real App Group store, the note list would
contain whatever the developer had typed, and a test that adds a note would leave it behind. FieldNotes therefore
recognises one launch argument, --uitesting, which the test sets through launchArguments before launching.
AppServices, introduced on Background Execution, reads it when it
creates the app’s one NoteStore. Only its initializer changes:
// FieldNotes/AppServices.swift (updated: init() only; the type's properties and attributes are unchanged)
private init() {
// UI tests launch with --uitesting: an empty in-memory store that never touches disk or iCloud.
let isUITesting = ProcessInfo.processInfo.arguments.contains("--uitesting")
let persistence: SwiftDataNotePersistence
do {
if isUITesting {
persistence = SwiftDataNotePersistence(modelContainer: try .fieldNotes(inMemory: true))
} else {
persistence = try SharedNoteAccess.persistence()
}
} catch {
// As on the SwiftData page: a store that can't be opened is a deployment error, not a runtime state.
fatalError("FieldNotes could not open its store: \(error)")
}
self.persistence = persistence
if isUITesting {
self.store = NoteStore(syncer: NoOpNoteSyncer(), persistence: persistence)
} else {
self.store = NoteStore(syncer: CloudNoteSyncer(), persistence: persistence)
}
}
Arguments set in launchArguments arrive in the app’s ProcessInfo.processInfo.arguments (and CommandLine.arguments).
Because AppServices.shared is created before the first scene appears, every view, delegate and intent that goes
through it sees the in-memory store. Code that opens the App Group store itself through SharedNoteAccess, such as
the widgets, NoteEntityQuery and TogglePinIntent, still reads the real store. That’s why the App Intents tests
(FieldNotesShortcutsTests and FieldNotesIntentTests) launch without --uitesting.
This is deliberate, not an oversight. FieldNotes has no separate App Intents extension: its intents are compiled
into the app target (and, for the shared ones, into the widget extensions too), so when Siri or Shortcuts run
TogglePinIntent they run it in the app’s own process. What makes NoteEntityQuery, TogglePinIntent and the
widgets read the real store is that they open the App Group store directly through SharedNoteAccess instead of
going through AppServices.shared, so --uitesting never reaches them. The widgets additionally run in their own
extension process, which WidgetKit launches and which never receives the arguments an
launchArguments
array sets on the app. A UI test that exercises one of those surfaces therefore seeds and cleans up that store
itself — for example by driving the app, launched without --uitesting, to create and remove the notes the test
needs — rather than relying on the in-memory store --uitesting provides.
The argument isn’t limited to Debug builds on purpose: the launch performance test below runs a Release build. It gives no access to data, only an empty store, so shipping it is harmless.
A first UI test
NoteCaptureUITests captures a note and checks that it appears in the list. continueAfterFailure = false stops the
test at the first failure, because every later step depends on the earlier ones. The XCUIAutomation APIs are
main-actor isolated, so the test methods are @MainActor.
// FieldNotesUITests/NoteCaptureUITests.swift
import XCTest
final class NoteCaptureUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
/// Every UI test starts from an empty, in-memory store (see AppServices.init()).
@MainActor
private func launchFieldNotes() -> XCUIApplication {
let app = XCUIApplication()
app.launchArguments.append("--uitesting")
app.launch()
return app
}
@MainActor
func testCapturedNoteAppearsInTheList() throws {
let app = launchFieldNotes()
app.buttons["capture-button"].firstMatch.tap()
let titleField = app.textFields["note-title-field"]
XCTAssertTrue(titleField.waitForExistence(timeout: 5))
titleField.tap()
titleField.typeText("Heron at the weir")
let save = app.buttons["save-note-button"]
XCTAssertTrue(save.wait(for: \.isEnabled, toEqual: true, timeout: 2))
save.tap()
let list = app.descendants(matching: .any)["note-list"]
XCTAssertTrue(list.staticTexts["Heron at the weir"].waitForExistence(timeout: 5))
}
}
See Apple Developer Documentation — XCUIApplication and Apple Developer Documentation -- wait(for:toEqual:timeout:).
A few habits keep UI tests reliable:
-
Wait, don’t sleep.
waitForExistence(timeout:)returns as soon as the element appears;wait(for:toEqual:timeout:)does the same for any property, such asisEnabled. A fixedsleepis either too short on a slow CI machine or wasted time everywhere else. -
Query by identifier, and keep queries short.
app.buttons["capture-button"]survives layout changes;app.navigationBars.element(boundBy: 0).buttons.element(boundBy: 2)doesn’t.firstMatchstops at the first match instead of requiring exactly one. -
Query by element type where the type is stable. A SwiftUI
Listis a collection view on iOS and an outline on macOS, sodescendants(matching: .any)finds"note-list"on both. -
Check user-visible results, such as the new title in the list, not implementation details.
Recording interactions
Writing queries by hand is the slow part of a UI test. Xcode can record them: put the cursor inside a test method in a UI testing target, click the record button at the edge of the editor, and use the app. Xcode launches the app, adds a query and an action for every interaction, and stops when you click the button again. Each recorded query has a disclosure menu of alternatives that locate the same element; pick the identifier-based one and double-click it. The first time, macOS asks to give Xcode Helper the Accessibility permission in System Settings.
A raw recording of the capture flow looks like this, before the queries are switched to identifiers:
// As recorded: queries by label, which break in the German configuration
let app = XCUIApplication()
app.activate()
app.buttons["Capture"].tap()
app.textFields["Title"].tap()
app.textFields["Title"].typeText("Heron at the weir")
app.buttons["Save"].tap()
Recording produces actions only. The assertions, which make it a test, are added by hand afterwards. A recorded test with no assertions still has value: it passes only if every interaction is still possible and the app doesn’t crash.
Accessibility audits in UI tests
performAccessibilityAudit(for:_:) checks the screen that is currently visible for missing labels, low contrast,
clipped text, small hit targets and Dynamic Type problems, and fails the test for each issue it finds.
Accessibility audits the note list in AccessibilityAuditTests; NoteCaptureUITests
also audits the composer, which the list audit never opens:
// FieldNotesUITests/NoteCaptureUITests.swift (continued, same file)
extension NoteCaptureUITests {
@MainActor
func testComposerPassesAccessibilityAudit() throws {
let app = launchFieldNotes()
app.buttons["capture-button"].firstMatch.tap()
XCTAssertTrue(app.textFields["note-title-field"].waitForExistence(timeout: 5))
try app.performAccessibilityAudit() // all audit types
}
}
See Apple Developer Documentation -- performAccessibilityAudit(for:_:) and WWDC23 — Perform accessibility audits for your app.
The extension can call the private launchFieldNotes() because it is in the same file.
Screenshots, video and attachments
A failing UI test is much easier to diagnose with a picture. There are two ways to get one:
-
Automatically. In a test plan configuration, Automatic Screen Capture decides whether the UI test runner captures media and whether media of passing tests is deleted, and Preferred Capture Format chooses video or screenshots (Xcode 26 and later). By default, media is kept only for failing runs; choose On, and keep all to keep everything, for example to reuse the video in documentation. The test report plays the video of each run, with the test’s steps alongside.
-
Explicitly.
XCTAttachment(screenshot:)wraps a screenshot of the app, a window or any element. Setlifetimeto.keepAlwaysto keep it even when the test passes.
// In testCapturedNoteAppearsInTheList, after typing the title
let composer = XCTAttachment(screenshot: app.screenshot())
composer.name = "Composer with a title"
composer.lifetime = .keepAlways
add(composer)
See Apple Developer Documentation — Adding attachments to tests, activities, and issues and Apple Developer Documentation -- init(screenshot:).
Attachments are saved in the result bundle (.xcresult) that every test run produces, so they are available from CI
as well. The Localization Screenshots option of a configuration is a different feature: it collects screenshots for
translators, as described on Localization with String Catalogs.
Performance Tests
A performance test is an XCTest method that calls measure(metrics:block:). XCTest runs the block
iterationCount + 1 times (set through XCTMeasureOptions), discards the first, warm-up iteration, records each
metric for the rest, and compares the average with a baseline. The test fails when the
result is worse than the baseline by more than the allowed standard deviation. The first run has no baseline: click
the gray icon next to measure in the editor, then Set Baseline, and edit Max STDDEV if the default is too
strict. Baselines are stored per device in the project, so commit them.
XCTClockMetric records elapsed wall-clock time. FieldNotes measures NoteStore.togglePin(_:) on a large
library, because it searches the notes array and runs on the main actor, where slowness becomes a visible hitch:
// FieldNotesTests/NoteStorePerformanceTests.swift (an XCTest class in the Swift Testing target)
import XCTest
import FieldNotesModel
import FieldNotesServices
final class NoteStorePerformanceTests: XCTestCase {
@MainActor
func testTogglingAPinInALargeLibrary() {
let notes = (0..<10_000).map { Note(title: "Survey note \($0)") }
let store = NoteStore(notes: notes)
let last = notes[notes.count - 1]
let options = XCTMeasureOptions()
options.iterationCount = 10
measure(metrics: [XCTClockMetric(), XCTCPUMetric()], options: options) {
store.togglePin(last)
}
}
}
See Apple Developer Documentation -- measure(metrics:options:block:) and Apple Developer Documentation — XCTClockMetric.
XCTApplicationLaunchMetric measures app launch, from process start to the first frame, or until the app is
responsive with waitUntilResponsive: true. It belongs in a UI test, since it launches the app each iteration:
// FieldNotesUITests/LaunchPerformanceTests.swift
import XCTest
final class LaunchPerformanceTests: XCTestCase {
@MainActor
func testLaunchUntilResponsive() {
measure(metrics: [XCTApplicationLaunchMetric(waitUntilResponsive: true)]) {
let app = XCUIApplication()
app.launchArguments.append("--uitesting") // same empty store every iteration
app.launch()
}
}
}
Performance numbers are only meaningful under release conditions. Apple’s guidance is to build for testing with the
Release configuration, turn off Debug executable in the scheme, and run from a test plan with code coverage and
the sanitizers off. FieldNotes therefore keeps its performance tests out of FieldNotes.xctestplan and runs
them from a second, small plan, FieldNotesPerformance.xctestplan, which contains only the two performance classes.
When a performance test fails, Control-click its icon in the gutter and choose Profile to open it in Instruments; see
Performance and Instruments.
Test Plans
A test plan (.xctestplan) is a file in the project that says which tests to run and how: the test targets,
the tests or tags to include or exclude, and one or more configurations. Xcode runs the plan’s tests once for each
configuration. A scheme has one default plan, which ⌘U runs, and may have others.
FieldNotes.xctestplan includes FieldNotesTests and FieldNotesUITests and has three configurations:
| Configuration | Settings |
|---|---|
Default |
Code Coverage on for the |
German |
As Default, plus Application Language German and Application Region Germany: longer strings, other date and number formats. Localization is covered on Localization with String Catalogs. |
Sanitizers |
Address Sanitizer and Undefined Behavior Sanitizer on, code coverage off, screen capture off. Thread Sanitizer can’t be combined with Address Sanitizer, so it is enabled in the scheme when needed; see Debugging with Xcode and LLDB. |
See Apple Developer Documentation — Improving code assessment by organizing tests into test plans and Apple Developer Documentation — Diagnosing memory, thread, and crash issues early.
The plan is edited in Xcode’s test plan editor (Product > Test Plan > Edit Test Plan), which writes a JSON file. Apple doesn’t document the file format, and the keys can change between Xcode versions, so edit plans in the editor and review the JSON in code review. A trimmed excerpt of what the editor writes for the configurations above looks like this:
{
"configurations" : [
{ "id" : "…", "name" : "Default", "options" : { } },
{ "id" : "…", "name" : "German", "options" : { "language" : "de", "region" : "DE" } },
{ "id" : "…", "name" : "Sanitizers",
"options" : { "addressSanitizer" : { "enabled" : true }, "undefinedBehaviorSanitizerEnabled" : true } }
],
"defaultOptions" : {
"testRepetitionMode" : "retryOnFailure",
"maximumTestRepetitions" : 3
},
"testTargets" : [
{ "parallelizable" : true,
"target" : { "containerPath" : "container:FieldNotes.xcodeproj", "identifier" : "…", "name" : "FieldNotesTests" } },
{ "target" : { "containerPath" : "container:FieldNotes.xcodeproj", "identifier" : "…", "name" : "FieldNotesUITests" } }
],
"version" : 1
}
Tags, repetitions and parallel testing
Tags. A test plan can include or exclude Swift Testing tests by tag (Xcode 16 and later): in the plan’s Tests
pane, type tag names such as persistence in Include Tags or Exclude Tags, and choose Any Tag or All Tags.
FieldNotes's pull-request plan excludes sync, so no test needs iCloud; the tags themselves are declared on
Unit Testing with Swift Testing. Individual suites, functions and
even single arguments of a parameterized test can also be unchecked in the plan’s outline.
Repetitions. Test Repetition Mode runs each test once, a fixed number of times, until it fails, or until it passes, up to Maximum Test Repetitions. "Until failure" is how to reproduce a flaky test; "Retry on failure" keeps an occasional UI-test hiccup from failing the build, but a test that needs retries is still a bug to fix. Relaunch Tests for Each Repetition starts a fresh process each time.
Parallelization. XCTest can split a test target across several clones of a simulator (or several processes on the
Mac), enabled per target in the plan (Execute in parallel). Swift Testing runs tests in parallel inside one process
anyway. UI tests parallelize well because each clone has its own app container, which is another reason for
--uitesting's isolated store. Execution Order: Random exposes tests that only pass after another test has run.
The same controls exist on the command line:
# Which plans does the scheme have?
xcodebuild -project FieldNotes.xcodeproj -scheme FieldNotes -showTestPlans
# Run one configuration of the plan, retrying failed tests up to 3 times, on 3 parallel simulator clones
xcodebuild test -project FieldNotes.xcodeproj -scheme FieldNotes \
-testPlan FieldNotes -only-test-configuration German \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-retry-tests-on-failure -test-iterations 3 \
-parallel-testing-enabled YES -parallel-testing-worker-count 3
# Reproduce a flaky UI test: run it until it fails, at most 50 times
xcodebuild test -project FieldNotes.xcodeproj -scheme FieldNotes \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:FieldNotesUITests/NoteCaptureUITests/testCapturedNoteAppearsInTheList \
-run-tests-until-failure -test-iterations 50
Figure: one test plan, several destinations, one report
Each configuration runs on each destination named on the command line or chosen in Xcode. The runs land in one result bundle, which feeds both the test report and the coverage report:
FieldNotesTests + FieldNotesUITests"] Plan --> Default["Default
coverage, video on failure"] Plan --> German["German
language de, region DE"] Plan --> San["Sanitizers
ASan + UBSan"] subgraph Destinations direction TB iPhone["iPhone 17 simulator"] iPad["iPad simulator"] Mac["My Mac"] end Default --> Destinations German --> Destinations San --> Destinations Destinations --> Runs["3 configurations x 3 destinations
= 9 runs of every included test"] Runs --> Bundle[("FieldNotes.xcresult")] Bundle --> Report["Test report
failures, retries, videos,
screenshots, attachments"] Bundle --> Coverage["Coverage report
per target, file and function"]
# The whole matrix: every configuration of the plan on three destinations
xcodebuild test -project FieldNotes.xcodeproj -scheme FieldNotes -testPlan FieldNotes \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M5)' \
-destination 'platform=macOS' \
-resultBundlePath build/FieldNotes.xcresult
Simulator names differ between Xcode versions; xcrun simctl list devices available shows the ones installed. In
Xcode Cloud, the same plan and destinations are chosen in the workflow’s test action, as shown on
CI/CD with Xcode Cloud and the Command Line.
Code Coverage
Code coverage records which lines ran during the tests. Enable it in the plan: Configurations tab, Shared Settings or one configuration, Code Coverage, then check Gather coverage for and choose the targets. After a run, the Coverage pane of the Report navigator lists each target, file and function with its percentage, and Editor > Code Coverage annotates the source: each line shows how often it ran, and code that never ran is highlighted.
# Collect coverage into a result bundle, then read it
xcodebuild test -project FieldNotes.xcodeproj -scheme FieldNotes -testPlan FieldNotes \
-only-test-configuration Default \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-enableCodeCoverage YES -resultBundlePath build/FieldNotes.xcresult
xcrun xccov view --report --only-targets build/FieldNotes.xcresult # one line per target
xcrun xccov view --report --json build/FieldNotes.xcresult > build/coverage.json
Coverage tells you what the tests didn’t run, not whether the tests that did run check anything. Use it to find
untested branches, such as an error path in NoteStore.load(), and then write a test for them. Keep in mind that:
-
skipped tests don’t count, but tests that ran with a known issue or an expected failure do;
-
gathering coverage slows the code down, linearly, so keep it off for performance tests;
-
UI tests produce coverage too, which can make a screen look tested when only its happy path was exercised.
FieldNotes doesn’t gate merges on a coverage percentage. It reviews the coverage of the files a change touches, and
the FieldNotesServices module, which holds the logic, is expected to be well covered.
Snapshot Testing Without a Snapshot Framework
Apple ships no snapshot-testing framework: nothing in XCTest or Swift Testing compares a rendered view with a reference image. FieldNotes covers the same need with three built-in tools:
-
Previews for reviewing views by eye, in light and dark mode, at large text sizes and in German, while developing; see Xcode Projects, Targets, Schemes and Previews.
-
Screenshots and video from UI tests, kept for every run in the Default configuration of a release branch, to compare screens between versions.
-
Image attachments in Swift Testing (Xcode 26.4 and later): a test renders a view with
ImageRendererand attaches the image, so each run’s report contains what the view looked like.
// FieldNotesTests/TagChipSnapshotTests.swift
import CoreGraphics
import SwiftUI
import Testing
import FieldNotesUI
@MainActor
@Suite(.tags(.ui))
struct TagChipSnapshotTests {
@Test(arguments: [false, true])
func rendersTagChip(isSelected: Bool) throws {
let renderer = ImageRenderer(content: TagChip("birds", isSelected: isSelected).padding())
renderer.scale = 2
let image = try #require(renderer.cgImage)
Attachment.record(image, named: isSelected ? "tag-chip-selected.png" : "tag-chip.png")
#expect(image.width > 0 && image.height > 0)
}
}
#Preview("Tag chips") {
HStack {
TagChip("birds")
TagChip("otters", isSelected: true)
}
.padding()
}
See Apple Developer Documentation — Attachments, Apple Developer Documentation — ImageRenderer and Apple Developer Documentation — Previews in Xcode.
These attachments are for people to look at; the test doesn’t compare pixels. Third-party libraries, such as Point-Free’s swift-snapshot-testing, add reference-image comparison on top of XCTest and Swift Testing if a project needs it; they are not Apple frameworks and pixel comparisons are sensitive to the OS version and simulator used.
Platform availability
The table lists what FieldNotes uses on this page. "Yes" means available on every release FieldNotes supports (26 and later). Test plans, coverage and the command-line tools are Xcode features and work for every destination that can run the kind of test involved.
| API or feature | iOS | iPadOS | macOS | watchOS | visionOS |
|---|---|---|---|---|---|
|
Yes |
Yes |
Yes |
Yes |
Not available (visionOS SDK apps) |
|
Yes |
Yes |
Yes |
Yes |
Not available |
|
Yes |
Yes |
Yes |
Yes |
Not available |
|
Yes |
Yes |
Yes |
Yes |
Yes |
|
Yes |
Yes |
Yes |
Yes |
Not available |
Test plans: configurations, tags, repetitions, parallel testing |
Yes |
Yes |
Yes |
Yes |
Yes |
Code coverage, |
Yes |
Yes |
Yes |
Yes |
Yes |
Swift Testing image attachments with |
Yes |
Yes |
Yes |
Yes |
Yes |
References
-
Apple Developer Documentation — Recording UI automation for testing
-
Apple Developer Documentation -- performAccessibilityAudit(for:_:)
-
Apple Developer Documentation -- accessibilityIdentifier(_:)
-
Apple Developer Documentation — Adding attachments to tests, activities, and issues
-
Apple Developer Documentation — Writing and running performance tests
-
Apple Developer Documentation -- measure(metrics:options:block:)
-
Apple Developer Documentation — Improving code assessment by organizing tests into test plans
-
Apple Developer Documentation — Diagnosing memory, thread, and crash issues early
-
Apple Developer Documentation — Running tests and interpreting results
-
Apple Developer Documentation — Determining how much code your tests cover
-
WWDC25 — Record, replay, and review: UI automation with Xcode