dánwòdánwò

Platforms

Native Android apps

Test a Jetpack Compose or Views app on an Android emulator, built from its own Gradle project, with no change to its code.

danwo tests a native Android app, written with Jetpack Compose or Views, the same way it tests an Expo app. It builds your Gradle project, routes the emulator's traffic through its proxy, and runs your tests, invariants and fault matrix against it.

You don't change the app's code. It keeps calling its real hosts over HTTPS. Android apps trust only the system's certificates by default, so danwo's build adds a network security config that also trusts the one it installs on the emulator. Your project's files are not touched.

Build the app

npx @danwo/cli build-android android
built android/.danwo/builds/56bf08b42328c942.apk (dev.danwo.examples.compose) in 108 s (log: android/.danwo/builds/build-android.log)

android is the folder with your Gradle settings file and gradlew. danwo:

  1. runs your wrapper, ./gradlew assembleRelease, with Android Studio's JDK when JAVA_HOME is unset;
  2. hands Gradle an init script that adds danwo's network config to the app module, for this build only;
  3. signs an unsigned APK with the debug key, so it installs;
  4. reads the package name from the APK.

Gradle runs with --no-daemon and compiles Kotlin in its own process, so no daemon is left running. An unchanged project isn't built again: the build is keyed by a hash of its sources.

OptionWhat it doesDefault
--variant <name>the variant to assemble: debug, freeReleaserelease
--forcebuilds again even when the sources are the sameoff
--verboseprints Gradle's outputoff

What the build adds

The network config trusts the system's certificates and the user-installed ones, where danwo puts each run's, and allows plain HTTP:

the network config danwo adds
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" />
      <certificates src="user" />
    </trust-anchors>
  </base-config>
</network-security-config>

It replaces any networkSecurityConfig your app sets, in danwo's build only. The init script adds it through the Android Gradle plugin's variant API (sources.res and sources.manifests); it is checked with AGP 9.4.

This build trusts user-installed certificates. Use it only for testing; ship your store builds from your own Gradle tasks.

Bind the emulator in the config

buildAndroid from @danwo/mobile does the same build from your config. It returns the APK and its package for withAndroidDevice:

danwo.config.ts · the device
const build = await buildAndroid({ project: "android" })

return withAndroidDevice(backend, {
  avd: "danwo_spike",
  worker: workerIndex,
  app: { package: build.package ?? "dev.danwo.examples.compose", apk: build.apk },
})

Route your API's production host to the local copy, as on iOS, so the app calls the URL it ships with. The full config is in examples/with-compose.

Make the screen findable

Compose reports a testTag to the accessibility tree only when the screen asks it to. Set testTagsAsResourceId once, at the root:

WaitlistScreen.kt · the root
Surface(modifier = Modifier.fillMaxSize().semantics { testTagsAsResourceId = true }) {
    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = { Text("Work email") },          // getByLabel("Work email")
        modifier = Modifier.testTag("email"),    // getByTestId("email")
    )
}
ComposeLocator
Modifier.testTag, with testTagsAsResourceIdgetByTestId
a text field's labelgetByLabel
Button { Text("Join") }getByRole("button", { name: "Join" })
Modifier.semantics { heading() }getByRole("heading")
Text("You're on the list")getByText

In Views, the view's android:id is its test id and contentDescription its label.

What reaches the proxy

WhatReaches the proxy
HttpURLConnection, OkHttp, Ktor on OkHttpyes: they use the device's proxy
a Custom Tabyes
an app that pins its certificatesno: turn pinning off in the build you test
a client with its own proxy settingonly if it follows the system's

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI