visionOS and RealityKit Testing: SwiftUI Spatial Components and Reality Composer Pro
visionOS is Apple's operating system for Apple Vision Pro. It runs SwiftUI apps in an immersive spatial environment alongside traditional window-based apps. Testing visionOS presents unique challenges: RealityKit entities exist in 3D space, gesture detection involves eye gaze and hand gestures, and the full immersive experience cannot run on any device except the physical headset or the visionOS Simulator (which requires Apple Silicon with macOS 14+).
What You Can Test
Before diving in, set realistic expectations:
| Test type | Where it runs |
|---|---|
| Unit tests (pure Swift logic) | macOS, any simulator |
| SwiftUI component snapshots | visionOS Simulator |
| RealityKit entity/component logic | visionOS Simulator |
| Reality Composer Pro scene loading | visionOS Simulator |
| Full immersive space interactions | visionOS Simulator only |
| Physical Vision Pro UI testing | ❌ Not supported |
Project Setup
Add a visionOS test target:
- File → New → Target → visionOS → Unit Testing Bundle
- Link it to your visionOS app target
- Select the Apple Vision Pro Simulator as the destination
Minimum requirement: Xcode 15+, macOS 14 Sonoma, Apple Silicon Mac.
Unit Testing RealityKit Logic
RealityKit uses an Entity-Component-System (ECS) architecture. Keep component logic in pure Swift structures — these are the easiest to test.
Testing Custom Components
import RealityKit
import XCTest
@testable import SpaceApp
struct OrbitalComponent: Component {
var radius: Float
var angularSpeed: Float // radians per second
var currentAngle: Float = 0
mutating func advance(by deltaTime: Float) {
currentAngle += angularSpeed * deltaTime
if currentAngle > .pi * 2 {
currentAngle -= .pi * 2
}
}
var position: SIMD3<Float> {
SIMD3(radius * cos(currentAngle), 0, radius * sin(currentAngle))
}
}
class OrbitalComponentTests: XCTestCase {
func testPositionAtZeroAngle() {
let component = OrbitalComponent(radius: 1.0, angularSpeed: 1.0)
XCTAssertEqual(component.position.x, 1.0, accuracy: 0.001)
XCTAssertEqual(component.position.z, 0.0, accuracy: 0.001)
}
func testAdvanceByQuarterPeriod() {
var component = OrbitalComponent(radius: 1.0, angularSpeed: 1.0)
component.advance(by: .pi / 2) // quarter turn
XCTAssertEqual(component.position.x, 0.0, accuracy: 0.001)
XCTAssertEqual(component.position.z, 1.0, accuracy: 0.001)
}
func testAngleWrapsAroundFullCircle() {
var component = OrbitalComponent(radius: 1.0, angularSpeed: 1.0)
component.advance(by: .pi * 2 + 0.1)
XCTAssertLessThan(component.currentAngle, .pi * 2)
}
}These tests run without any simulator because OrbitalComponent is pure math.
Testing Entity Hierarchies
For tests that touch Entity and ModelEntity, you need the visionOS Simulator:
import RealityKit
import XCTest
@testable import SpaceApp
class EntityHierarchyTests: XCTestCase {
@MainActor
func testChildEntityAddedToParent() async {
let parent = Entity()
let child = ModelEntity()
parent.addChild(child)
XCTAssertEqual(parent.children.count, 1)
XCTAssertTrue(parent.children.contains(child))
XCTAssertIdentical(child.parent, parent)
}
@MainActor
func testEntityTransformIsIdentityByDefault() {
let entity = Entity()
XCTAssertEqual(entity.transform.scale, SIMD3<Float>(1, 1, 1))
XCTAssertEqual(entity.position, .zero)
}
}Note @MainActor — RealityKit entity operations must run on the main actor in visionOS.
Testing Reality Composer Pro Scenes
Reality Composer Pro creates .usda scene files packaged in a .realitycomposerpro bundle. Loading and inspecting these is testable:
class SceneLoadingTests: XCTestCase {
@MainActor
func testSolarSystemSceneLoads() async throws {
let scene = try await Entity(named: "SolarSystem", in: Bundle.main)
XCTAssertNotNil(scene)
}
@MainActor
func testPlanetEntitiesExistInScene() async throws {
let scene = try await Entity(named: "SolarSystem", in: Bundle.main)
let earth = scene.findEntity(named: "Earth")
let mars = scene.findEntity(named: "Mars")
XCTAssertNotNil(earth, "Earth entity should exist in scene")
XCTAssertNotNil(mars, "Mars entity should exist in scene")
}
@MainActor
func testEarthHasOrbitalComponent() async throws {
let scene = try await Entity(named: "SolarSystem", in: Bundle.main)
let earth = try XCTUnwrap(scene.findEntity(named: "Earth"))
XCTAssertNotNil(earth.components[OrbitalComponent.self])
}
}This pattern catches regressions when scene files are edited in Reality Composer Pro — if someone renames or deletes an entity, the test fails.
Testing SwiftUI Spatial Views
visionOS SwiftUI adds spatial modifiers like .glassBackgroundEffect(), RealityView, and Model3D. ViewInspector doesn't fully support visionOS-specific modifiers, but you can test the view model that drives the view:
View Model Pattern
@Observable
class SolarSystemViewModel {
var selectedPlanet: Planet?
var isImmersiveSpaceOpen = false
var orbitSpeed: Float = 1.0
func selectPlanet(_ planet: Planet) {
selectedPlanet = planet
}
func toggleImmersiveSpace() {
isImmersiveSpaceOpen.toggle()
}
func adjustSpeed(by delta: Float) {
orbitSpeed = max(0.1, min(5.0, orbitSpeed + delta))
}
}
class SolarSystemViewModelTests: XCTestCase {
func testSelectingPlanetUpdatesSelection() {
let vm = SolarSystemViewModel()
vm.selectPlanet(.earth)
XCTAssertEqual(vm.selectedPlanet, .earth)
}
func testOrbitSpeedClampsAtMinimum() {
let vm = SolarSystemViewModel()
vm.adjustSpeed(by: -100)
XCTAssertEqual(vm.orbitSpeed, 0.1)
}
func testOrbitSpeedClampsAtMaximum() {
let vm = SolarSystemViewModel()
vm.adjustSpeed(by: 100)
XCTAssertEqual(vm.orbitSpeed, 5.0)
}
func testImmersiveSpaceToggles() {
let vm = SolarSystemViewModel()
XCTAssertFalse(vm.isImmersiveSpaceOpen)
vm.toggleImmersiveSpace()
XCTAssertTrue(vm.isImmersiveSpaceOpen)
vm.toggleImmersiveSpace()
XCTAssertFalse(vm.isImmersiveSpaceOpen)
}
}This approach tests all the logic that drives your spatial UI without touching SwiftUI or RealityKit at all.
Gesture Testing Limitations
visionOS gestures are driven by eye gaze (to determine where the user is looking) combined with pinch gestures (hand tracking). XCUITest cannot simulate:
- Eye gaze direction
- Hand positions in 3D space
- Spatial tap gestures driven by combined eye + hand input
What you can test in the visionOS Simulator with XCUITest:
- Buttons and interactive UI elements reachable via mouse click (in the simulator, mouse simulates pinch)
- Text field input
- Navigation between windows
import XCTest
class VisionOSUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testPlanetDetailViewOpens() {
app.buttons["Earth"].tap()
XCTAssertTrue(app.staticTexts["Earth Details"].waitForExistence(timeout: 3))
}
}CI Setup
xcodebuild test \
-scheme SpaceApp \
-destination <span class="hljs-string">'platform=visionOS Simulator,name=Apple Vision Pro' \
-resultBundlePath TestResults.xcresultBoot the simulator before running tests:
xcrun simctl boot "Apple Vision Pro"
xcrun simctl bootstatus <span class="hljs-string">"Apple Vision Pro" -bThe visionOS Simulator requires Apple Silicon and macOS 14+. GitHub Actions' macos-14 runner (M1) supports it.
Key Points
- Pure Swift ECS component logic is fully testable without any simulator
Entity,ModelEntity, and RealityKit scene loading tests require the visionOS Simulator and@MainActor- Test Reality Composer Pro scenes by loading the
.usdascene and asserting entity names and components — catches renames and deletions - Use view model patterns to test all spatial UI logic without touching SwiftUI or RealityKit
- Eye gaze and hand gesture simulation are not available in XCUITest — test only button/click interactions in UI tests
- visionOS Simulator requires Apple Silicon + macOS 14; use
macos-14GitHub Actions runners