| 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.
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 }
}
// 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))
}
}
elide test
elide test --select=class:com.example.greeter.GreetingTest
dependencies.android.robolectricadds 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 whencompileSdkis newer. - The test JVM gets the
--add-opensand--enable-native-accessflags Robolectric needs on a current JDK, after the project’s ownjvm.flags. - Test-only libraries contribute their manifests: add
androidx.compose.ui:ui-test-manifesttotestPackagesto launchComponentActivityfrom 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.
android {
namespace = "com.example.app"
compileSdk = 36
hostTests = "app-dev"
}
Host test selection, reporters and coverage work as for any JVM test (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.
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
dependencies {
android {
test { runner = "1.6.2"; modules { "ext-junit" }; espresso { "core" } }
}
}
// 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
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:
- builds the app and an implicit
app-androidTestAPK that instruments it; - uninstalls and reinstalls both, so every run starts from empty app data;
- runs the tests with
AndroidJUnitRunner, reporting each as it finishes, with a failure’s stack trace; - 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., merged with src/ |
| 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:
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:
android {
namespace = "com.example.app"
compileSdk = 36
devices {
["pixel"] {}
["small"] { deviceProfile = "small_phone"; api = 34 }
}
}
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); 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
- 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).
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).