Skip to content

Android: introduce unit tests and Kotlin - #38

Draft
ofalvai wants to merge 1 commit into
masterfrom
push-rzlklynysuys
Draft

Android: introduce unit tests and Kotlin#38
ofalvai wants to merge 1 commit into
masterfrom
push-rzlklynysuys

Conversation

@ofalvai

@ofalvai ofalvai commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Why

We want unit test coverage for the Android native module, and to be able to write new Android code (tests first, then prod code) in Kotlin instead of Java. This PR introduces both, wires the resulting tests into CI, and along the way upgrades this legacy module's Gradle/AGP/Kotlin toolchain since it needed it to support Kotlin properly.

What

Kotlin support (android/app/build.gradle)

  • Applies the Kotlin Gradle plugin via the classic buildscript { classpath ... } + apply plugin form, reading the version from the consumer's own rootProject.ext.kotlinVersion first (most RN apps already set this for reanimated/gesture-handler/etc.) and falling back to a bundled default otherwise.
  • Below AGP 9, applies kotlin-android explicitly. On AGP 9+, skips it — AGP 9 provides Kotlin support built in, and applying the classic plugin on top of it fails. This mirrors how other RN libraries adapted for AGP9 (see the patches linked from the AGP v9 adoption RFC).
  • compileOptions bumped to JVM 17 (from 11), matching mise.toml and RN 0.76+'s own minimum JDK. Under AGP9's built-in Kotlin, the Kotlin JVM target defaults to this automatically (see Android's built-in Kotlin migration guide); below AGP9 it's still set explicitly via kotlinOptions.

First unit test (android/app/src/test/java/com/microsoft/codepush/react/CodePushUtilsTest.kt)

  • Two JUnit tests for CodePushUtils.appendPathComponent, written in Kotlin. Chosen as the first target because it's pure java.io.File logic with no RN-bridge JNI or Android-stub dependencies, so it runs under a plain JVM unit test without needing Robolectric.

Modernized Gradle defaults (android/app/build.gradle fallbacks only, android/build.gradle, android/gradle.properties, android/gradle/wrapper/gradle-wrapper.properties, Examples/CodePushDemo/android/gradle.properties)

  • DEFAULT_COMPILE_SDK_VERSION/DEFAULT_BUILD_TOOLS_VERSION/DEFAULT_TARGET_SDK_VERSION/DEFAULT_MIN_SDK_VERSION bumped to match RN's current template — these are fallbacks only, used solely when a consumer app doesn't set its own (which real RN apps always do), so this doesn't change behavior for any real consumer.
  • compileSdkVersion(int)compileSdk (AGP 9 removed the old method).
  • This repo's own standalone test harness (android/build.gradle, gradle.properties, wrapper) bumped to AGP 9.2.1 / Gradle 9.4.1, and opts out of AGP9's built-in Kotlin/new DSL via android.builtInKotlin=false/android.newDsl=false — matching what RN's own 0.87 app template does (this opt-out is itself temporary; it's removed starting AGP10). Applied the same opt-out to Examples/CodePushDemo for consistency.

CI (.github/workflows/ci-test.yml)

  • New android-unit-test job: fast, runs on ubuntu-latest, just npm install + ./gradlew :app:test.
  • android-test (the slow emulator-based e2e suite) now depends on android-unit-test, so a broken unit test fails fast instead of waiting on the much slower e2e run.
  • Both jobs use gradle/actions/setup-gradle@v6 for dependency/build caching, shared across jobs in the same workflow run; android-test sets cache-read-only: true since its Gradle build lives in a test app generated at runtime, not a project checked into the repo, so it has nothing meaningful to contribute back to the cache.

Decisions

  • Classic buildscript/apply plugin over the plugins { id(...) version(...) } DSL for Kotlin. The latter hard-fails the whole consumer build if any other subproject (e.g. reanimated) applies a different Kotlin version, since it enforces exact version matches per plugin id across the whole multi-project build. The classic form just resolves to a single version (highest wins), which is what a library consumed as live Gradle source via autolinking needs.
  • AGP-major-version check (agpMajor < 9) instead of probing for an already-registered kotlin extension. Verified empirically that AGP9's built-in Kotlin compilation stays active even when a consumer opts out of the new DSL via android.builtInKotlin=false, so the AGP version alone is enough to decide — no need to inspect what the consumer opted into. This is also the same pattern used by other RN libraries' AGP9 migration patches.
  • Only android/app/build.gradle reaches downstream consumers. RN autolinking includes it as a live Gradle subproject inside the consumer's own build (not a prebuilt binary), so it's the only file in android/ whose changes have any downstream effect. Everything else (android/build.gradle, gradle.properties, wrapper) governs only this repo's own standalone test harness.
  • Fail-fast job ordering over folding unit tests into the existing e2e job. android-unit-test runs in its own fast ubuntu-latest job gating the slow emulator job via needs:, rather than sharing a job for Gradle-cache convenience — a broken unit test now fails in seconds instead of after the full e2e run.

@ofalvai
ofalvai force-pushed the push-rzlklynysuys branch from dd451be to 3dd892e Compare August 18, 2026 18:20
@ofalvai
ofalvai force-pushed the push-rzlklynysuys branch from 3dd892e to 43475d9 Compare August 19, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant