Skip to content

Testing SceneView Apps

Strategies for testing 3D and AR features in your Android app.


Unit testing node logic

Business logic that drives your scene — model selection, animation state, anchor management — can be tested with standard JUnit tests. Keep scene logic in ViewModels or plain Kotlin classes.

// ViewModel
class SceneViewModel : ViewModel() {
    var selectedModel by mutableStateOf("helmet")
        private set

    var isAnimating by mutableStateOf(true)
        private set

    fun selectModel(name: String) { selectedModel = name }
    fun toggleAnimation() { isAnimating = !isAnimating }
}

// Test
@Test
fun `selecting a model updates state`() {
    val vm = SceneViewModel()
    vm.selectModel("sword")
    assertEquals("sword", vm.selectedModel)
}

Keep Filament out of unit tests

Filament requires native libraries and a GPU context. Don't instantiate Engine, ModelLoader, or any Filament objects in unit tests. Test the state logic, not the rendering.


Compose UI testing

Use composeTestRule to test the Compose UI around your scene — buttons, sliders, model pickers. You can't render the actual 3D scene in instrumented tests (no GPU in CI), but you can verify that state changes propagate.

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun modelPickerUpdatesSelection() {
    val vm = SceneViewModel()
    composeTestRule.setContent {
        ModelPickerUI(viewModel = vm)
    }

    composeTestRule.onNodeWithText("Sword").performClick()
    assertEquals("sword", vm.selectedModel)
}

Screenshot testing

For visual regression testing, use Paparazzi or Roborazzi. These render Compose UI without a device but cannot render Filament 3D content (they use layoutlib, not a real GPU).

What you can screenshot-test:

  • Compose UI overlays (buttons, HUD, model pickers)
  • Loading states (skeleton/placeholder before model loads)
  • Error states

What you cannot screenshot-test:

  • The 3D scene itself
  • AR camera feed
  • Filament rendering output

For 3D visual testing, use on-device screenshot tests with Shot or manual QA.


Instrumented testing

For on-device tests that exercise the full rendering pipeline:

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun sceneRendersWithoutCrash() {
    // Just verify the Scene composable doesn't throw
    composeTestRule.waitForIdle()
    // If we get here, the scene initialized successfully
}

@Test
fun tappingPlacesModel() {
    // Wait for scene to initialize
    composeTestRule.waitForIdle()

    // Simulate a tap
    composeTestRule.onRoot().performClick()

    // Verify state changed (model placed)
    // Check via ViewModel or test tag
}

AR tests require a physical device

AR features need a real camera and ARCore. Run AR instrumented tests on physical devices only — not emulators.


CI pipeline

What to run in CI

Check CI-safe? Tool
Unit tests (state logic) Yes JUnit
Compose UI tests Yes composeTestRule
Lint / ktlint Yes ./gradlew lint
Build verification Yes ./gradlew assembleDebug
Screenshot tests (UI only) Yes Paparazzi/Roborazzi
3D rendering tests No (needs GPU) On-device only
AR tests No (needs camera) Physical device only

Sample CI config

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
      - run: ./gradlew test              # Unit tests
      - run: ./gradlew lintDebug         # Lint
      - run: ./gradlew assembleDebug     # Build verification

Testing patterns

Mock the model loader

For tests that need a ModelInstance, create a fake:

// In test
val fakeInstance = mockk<ModelInstance>(relaxed = true)

// Pass to composable under test
ModelNode(modelInstance = fakeInstance, scaleToUnits = 1.0f)

Test animation state transitions

@Test
fun `animation toggles between walk and idle`() {
    val vm = SceneViewModel()

    assertEquals("Idle", vm.currentAnimation)

    vm.startWalking()
    assertEquals("Walk", vm.currentAnimation)

    vm.stopWalking()
    assertEquals("Idle", vm.currentAnimation)
}

Test AR anchor lifecycle

@Test
fun `clearing anchor removes placed model`() {
    val vm = ARViewModel()

    vm.placeAnchor(mockAnchor)
    assertTrue(vm.hasPlacedModel)

    vm.clearAnchor()
    assertFalse(vm.hasPlacedModel)
    assertNull(vm.currentAnchor)
}