# Android tests

| Tests | Live in | Run with | Run on |
| --- | --- | --- | --- |
| Host tests | `src/test` | `elide test` | The build machine's JVM, under Robolectric |
| Instrumented tests | `src/androidTest` | `elide test --device` | A device or emulator |

## Host tests

Host tests are JVM tests of an Android project. Under Robolectric they read the project's resources, assets and
merged manifest, as the Android Gradle Plugin's `isIncludeAndroidResources = true` makes them.

```pkl
amends "elide:project.pkl"

import "elide:Android.pkl" as Android

name = "greeter"

android {
  namespace = "com.example.greeter"
  compileSdk = 36
  minSdk = 30
  resources {
    strings { ["greeting"] = "Hello from Pkl" }
  }
}

dependencies {
  android {
    androidx { ["core-ktx"] = "1.15.0" }
    robolectric = "4.16"
  }
  maven {
    testPackages {
      "junit:junit:4.13.2"
      "androidx.test:core:1.7.0"
      "androidx.test.ext:junit:1.3.0"
    }
  }
}

artifacts {
  ["app"] = new Android.AndroidApk { release = false }
}
```

```kotlin
// src/test/kotlin/com/example/greeter/GreetingTest.kt
package com.example.greeter

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
@Config(sdk = [35])
class GreetingTest {
  @Test
  fun readsTheGeneratedString() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    assertEquals("Hello from Pkl", context.getString(R.string.greeting))
  }
}
```

```bash
elide test
elide test --select=class:com.example.greeter.GreetingTest
```

- `dependencies.android.robolectric` adds Robolectric and the JUnit vintage engine that runs its JUnit 4 runners.
- A test class names its runner: `@RunWith(AndroidJUnit4::class)` or `@RunWith(RobolectricTestRunner::class)`.
- Robolectric runs the API level the merged manifest targets unless `@Config(sdk = …)` says otherwise, and refuses
  a level it does not support; pin one it supports when `compileSdk` is newer.
- The test JVM gets the `--add-opens` and `--enable-native-access` flags Robolectric needs on a current JDK, after
  the project's own `jvm.flags`.
- Test-only libraries contribute their manifests: add `androidx.compose.ui:ui-test-manifest` to `testPackages` to
  launch `ComponentActivity` from Compose test rules.

Host tests run against one artifact of the project, linked a second time with the test libraries: `android.hostTests`
names it, else the first application, library or bundle the project declares.

```pkl
android {
  namespace = "com.example.app"
  compileSdk = 36
  hostTests = "app-dev"
}
```

Host test selection, reporters and coverage work as for any JVM test ([Testing](/docs/toolchain/testing)).

## Instrumented tests

`src/androidTest` is a source set in every Android project. Its tests run on a device, never on the host: `elide test`
does not compile them, and no artifact packages them.

```text
src/main/kotlin/com/example/app/MainActivity.kt
src/androidTest/kotlin/com/example/app/MainActivityTest.kt
src/androidTest/AndroidManifest.xml        # optional
src/androidTest/res, src/androidTest/assets # optional
```

```pkl
dependencies {
  android {
    test { runner = "1.6.2"; modules { "ext-junit" }; espresso { "core" } }
  }
}
```

```kotlin
// src/androidTest/kotlin/com/example/app/MainActivityTest.kt
package com.example.app

import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
  @Test
  fun launches() {
    ActivityScenario.launch(MainActivity::class.java).use { scenario ->
      assertEquals(Lifecycle.State.RESUMED, scenario.state)
    }
  }
}
```

`androidTest` sees `main`'s classes, its `R` and its dependencies, plus the instrumented-test scope:
`dependencies.android.test` and `dependencies.maven.androidTestPackages`. Neither the app nor host tests see that
scope.

### Run them

```bash
elide test --device                                  # the attached device, else the project's emulator
elide test --device=emulator-5554                    # that device
elide test --device=small                            # a device android.devices declares
elide test --device=all                              # every declared device
elide test --device --tests=com.example.app.MainActivityTest
elide test --device --tests=com.example.app.MainActivityTest#launches
```

A `--device` value is `all` first, then a declared device's name, then an attached device's serial. `--tests` takes
`<class>` or `<class>#<method>`, repeated or comma-separated, and needs `--device`; host tests are selected with
`--select`. Paths are refused with `--device`.

The run:

1. builds the app and an implicit `app-androidTest` APK that instruments it;
2. uninstalls and reinstalls both, so every run starts from empty app data;
3. runs the tests with `AndroidJUnitRunner`, reporting each as it finishes, with a failure's stack trace;
4. ends with a passed, failed and skipped count.

It fails when a test fails, when the run did not finish (the process crashed or the device went away; the running
test is named), and when no test ran, including when a `--tests` filter matches nothing.

### The test APK

| Of `app-androidTest` | Is |
| --- | --- |
| Classes | `androidTest`'s, and the libraries of its scope that the app does not already ship |
| Manifest | An `<instrumentation>` of `androidx.test.runner.AndroidJUnitRunner`, merged with `src/androidTest/AndroidManifest.xml` |
| Target | The app's `applicationId` |
| Package | The app's `applicationId` with `.test` appended |
| Signing | The app's own |

It instruments the first APK the project declares that is neither a test APK nor minified. `elide build
app-androidTest` builds it alone, as `.dev/artifacts/app-androidTest.apk`. Declaring an artifact named
`app-androidTest` replaces it.

### Declared test APKs

An APK declaring `instrumentation` is a test APK of its own, which `android-test-<artifact>` runs. A
self-instrumenting APK carries the tests, the code under test and the activities they start in one package:

```pkl
artifacts {
  ["app-tests"] = new Android.AndroidApk {
    applicationId = "com.example.app.test"
    release = false
    instrumentation {}
  }
}
```

`instrumentation.runner` defaults to `androidx.test.runner.AndroidJUnitRunner`; `instrumentation.targetPackage`
names another installed app to instrument. A target package the build did not install has its data cleared before
the run.

## Test devices

`android.devices` names virtual devices to run instrumented tests on, as Gradle's managed devices do:

```pkl
android {
  namespace = "com.example.app"
  compileSdk = 36
  devices {
    ["pixel"] {}
    ["small"] { deviceProfile = "small_phone"; api = 34 }
  }
}
```

```bash
elide test --device=pixel
elide test --device=all
elide build android-test-app-on-all --no-window --install-sdk   # headless CI, images installed
```

A device takes the fields `android.device` does ([Run and debug](/docs/toolchain/android/run#the-projects-emulator)); its AVD
defaults to `<project>-<device>`. A run creates the AVD if it is missing, boots it or adopts it if it is already
running, and stops it afterwards if it booted it, so a matrix does not outlive the build. Names are letters, digits,
`_` and `-`, starting with a letter; `all` is reserved.

## In CI

```yaml
- uses: elide-dev/setup-elide@v4.2.0
- run: elide install --accept-android-licenses
- run: elide test
- run: elide build android-test-app-on-all --no-window --install-sdk
```

Instrumented tests on Linux runners need KVM ([Headless hosts, CI and WSL](/docs/toolchain/android/run#headless-hosts-ci-and-wsl)).
`android/test` and `android/test/host` in `elide mcp` run the same targets for an agent and return every test's
outcome ([Editors and agents](/docs/toolchain/android/editors#agents)).
