Highlight
Apple introduces the AppIntentsTesting framework, which lets developers execute real App Intents across processes from XCUITest by using a bundle ID. You can validate intent execution, entity queries, Spotlight indexing, and view annotations without relying on mocks.
Core Content
Why App Intents used to be hard to test
Your app may integrate with Siri, Shortcuts, and Spotlight so users can operate it by voice or shortcuts. Testing those capabilities has always been difficult.
Option one: write UI tests that tap through the Shortcuts interface. As soon as a button moves, the test breaks. A single run takes tens of seconds and is extremely unstable in CI.
Option two: call perform() directly in code. This bypasses system parameter parsing, entity matching, and permission checks, so it does not test the real path.
The result: many teams simply do not test these flows, and only discover later from user feedback that Siri does not understand them, Spotlight cannot find them, or Shortcuts passes the wrong parameters.
What AppIntentsTesting does
Apple built the testing framework directly into the system. Test code lives in a standard XCUITest bundle and uses IntentDefinitions(bundleIdentifier:) to retrieve all intents, entities, and queries defined by the app. At runtime, the intent executes for real inside the app process, and the result is returned to the test process through IPC.
The test target does not import app code; it only passes the bundle ID. This forces tests through the real system route, including parameter parsing, entity lookup, and permission checks.
The cost is that there is no compile-time type checking. Parameter names and types in makeIntent(name:color:) do not get code completion, and typos fail only at runtime. Apple considers this tradeoff worthwhile: a little inconvenience while writing tests buys more reliable system-level integration tests.
Tests can cover system-level integrations too
AppIntentsTesting does more than test intent execution. It can also test whether Spotlight indexing works and whether view annotations pass the correct entity ID. These used to be black boxes that could not be tested at all.
Details
Execute your first intent test
(06:48)
Using the CreateCalendarIntent from the CometCal sample app, create a calendar and assert the return value:
import AppIntentsTesting
func testCreateCalendar() async throws {
let definitions = IntentDefinitions(bundleIdentifier: "com.example.apple-samplecode.CometCal")
let createCalendar = definitions.intents["CreateCalendarIntent"]
let result = try await createCalendar.makeIntent(
name: "Occupy Saturn",
color: "red"
).run()
XCTAssertEqual(try result.value.title, "Occupy Saturn")
}
Key points:
IntentDefinitionsis initialized with a bundle ID and does not depend on importing app code.intents["CreateCalendarIntent"]retrieves an intent definition through a string subscript.- Parameter names in
makeIntentmust match the definitions in the app, but the compiler does not check them. color: "red"is the raw string for anAppEnum, and the framework automatically converts the type.result.valueaccesses returned entity properties through dynamic member lookup.- The test runs in a separate process, while the app executes the intent in another process, so state is not shared.
Test entity string queries
(12:25)
When Shortcuts and Siri search for entities, they call EntityStringQuery. The following example implements it in a test-driven way:
func testEventStringQuery() async throws {
let results = try await eventEntityDefinition
.entities(matching: "Cosmic Ray")
XCTAssertEqual(results.count, 1)
XCTAssertEqual(try results[0].title, "Cosmic Ray Calibration")
}
Write the test first and watch it fail. Then implement the query:
struct EventEntityQuery: EntityStringQuery {
func entities(for identifiers: [EventEntity.ID]) async throws -> [EventEntity] {
// Look up by ID
}
func suggestedEntities() async throws -> [EventEntity] {
// Return a suggested list
}
func entities(matching string: String) async throws -> [EventEntity] {
try calendarManager.fetchEvents()
.filter { $0.title.localizedCaseInsensitiveContains(string) }
.map(\.entity)
}
}
Key points:
entities(matching:)executes the string query for real on device.- The returned
EventEntityarray can be asserted through dynamic member lookup. - Write the test first, then implement the query, so the TDD flow is complete.
- After the fix, searching for the same event in Shortcuts returns the correct result.
Chain multiple intents together
(15:42)
A core Shortcuts use case is passing the output of one intent into the next. Tests can simulate this flow:
func testCreateAndUpdateEvent() async throws {
let createResult = try await createEventDefinition.makeIntent(
title: "Asteroid Dodgeball Practice",
startDate: Date(),
isAllDay: false,
calendar: "Deep Space"
).run()
XCTAssertEqual(try createResult.value.title, "Asteroid Dodgeball Practice")
let updateResult = try await updateEventDefinition.makeIntent(
title: "Asteroid Dodgeball Rules Overview",
event: createResult.value
).run()
XCTAssertEqual(try updateResult.value.title, "Asteroid Dodgeball Rules Overview")
}
Key points:
calendar: "Deep Space"passes a string, and the App Intents runtime automatically calls theEntityStringQueryforCalendarEntityto match the first result.event: createResult.valuepasses the entity returned by the previous intent directly into the next intent.- Chaining simulates the way users connect actions by dragging them together in Shortcuts.
- The whole flow completes in one test: create, assert, update, and assert again.
Test-only intents
(17:45)
Tests need isolated, repeatable data environments. Use isDiscoverable = false and #if DEBUG to create intents that exist only for testing:
#if DEBUG
struct SeedSampleEventsIntent: AppIntent {
static let isDiscoverable = false
func perform() async throws -> some IntentResult {
// Create events from a known list
return .result()
}
}
#endif
Key points:
isDiscoverable = falseprevents the system from exposing this intent anywhere.#if DEBUGensures the release build does not contain test code.- It can be used to inject test data, jump to any screen, or reset app state.
- Even if the app UI changes, the test remains stable.
Test Spotlight indexing
(20:27)
After the app creates an event, that event should be indexed in Spotlight automatically. Verify this with spotlightQuery():
func testNewEventIndexedInSpotlight() async throws {
let before = try await eventEntityDefinition.spotlightQuery("Supernova Viewing Party")
XCTAssertTrue(before.isEmpty, "Event should not exist in Spotlight yet")
// Create the "Supernova Viewing Party" event
let after = try await eventEntityDefinition.spotlightQuery("Supernova Viewing Party")
XCTAssertEqual(after.count, 1)
XCTAssertEqual(try after[0].title, "Supernova Viewing Party")
}
Key points:
spotlightQuery(_:)communicates directly with Spotlight and returns the list of matching entities.- First assert that the event cannot be found before creation, then assert that it can be found afterward to prevent regressions.
- This test can catch silent bugs such as accidentally commenting out indexing code.
Test view annotations
(22:33)
View annotations tell Siri which entity is currently displayed on screen. If they are wrong, Siri may answer “What is this?” with the wrong information:
func testEventViewAnnotation() async throws {
try await openEventDefinition.makeIntent(target: "Morning Launch Briefing").run()
let app = XCUIApplication()
let title = app.staticTexts["Morning Launch Briefing"]
XCTAssertTrue(title.waitForExistence(timeout: 5))
let annotations = try await eventEntityDefinition.viewAnnotations()
XCTAssertEqual(annotations.count, 1, "Expected exactly one view annotation")
XCTAssertEqual(try annotations[0].entity.title, "Morning Launch Briefing")
}
Key points:
- First open the page with an intent, then use XCUI to confirm the UI rendered correctly.
viewAnnotations()retrieves the list of current screen entities reported by the system.annotations[0].entityaccesses entity properties through dynamic member lookup.- This test once helped the presenter discover a bug where
calendar.idhad been passed instead ofevent.id.
Key Takeaways
1. Put Siri automation tests into CI
If your app has Siri shortcuts, you can now write automated tests for core paths. Pick the most common voice command, execute it with makeIntent(...).run(), and assert the result. Run it on every commit so Siri regressions are caught immediately.
Entry API: IntentDefinitions(bundleIdentifier:) + intents["IntentName"].makeIntent(...).run()
2. Add regression tests for Spotlight indexing
If you use CoreSpotlight to index app content, write a test where spotlightQuery returns empty before content is created and returns one result afterward. This prevents indexing logic from being broken accidentally.
Entry API: entityDefinition.spotlightQuery("keyword")
3. Manage test data with test-only intents
Write a SeedDataIntent wrapped in #if DEBUG. Call it in setUp() to inject fixed data. Each test case can start from zero instead of depending on dirty data left by a previous test.
Entry API: static let isDiscoverable = false
4. Validate Siri screen awareness
If your app supports contextual commands such as “Siri, what is this?”, write a view annotation test. Open a specific page and assert that viewAnnotations() returns the correct entity ID.
Entry API: entityDefinition.viewAnnotations()
5. Use TDD to implement entity queries
The next time you implement EntityStringQuery, write the test for entities(matching:) first, see it fail, and then implement the query. It is much faster than manually verifying search inside Shortcuts.
Entry API: entityDefinition.entities(matching: "search term")
Related Sessions
- Build agentic apps with App Intents - Learn the new capabilities in App Intents and find more coverage points for testing
- What’s new in App Intents - Understanding the new App Intents APIs is the prerequisite for writing tests
- Go further with App Intents - Explore advanced App Intents usage and richer testing scenarios
- Design great widgets - Widgets also depend on App Intents to provide data, so the testing framework applies there too
- What’s new in Siri - The quality of Siri experiences depends on the correctness of App Intents
Comments
GitHub Issues · utterances