visionOS and RealityKit Testing: SwiftUI Spatial Components and Reality Composer Pro

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:

  1. File → New → Target → visionOS → Unit Testing Bundle
  2. Link it to your visionOS app target
  3. 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.xcresult

Boot the simulator before running tests:

xcrun simctl boot "Apple Vision Pro"
xcrun simctl bootstatus <span class="hljs-string">"Apple Vision Pro" -b

The 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 .usda scene 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-14 GitHub Actions runners

Read more