Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(android): add instrumentation test github action #4178

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions .github/workflows/_kotlin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ on:
workflow_call:
workflow_dispatch:

defaults:
run:
working-directory: ./kotlin/android

permissions:
contents: 'read'
id-token: 'write'
Expand All @@ -11,9 +15,6 @@ jobs:
static-analysis:
# Android SDK tools hardware accel is available only on Linux runners
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./kotlin/android
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
Expand All @@ -33,9 +34,6 @@ jobs:
build:
# Android SDK tools hardware accel is available only on Linux runners
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./kotlin/android
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
Expand All @@ -59,9 +57,8 @@ jobs:
KEYSTORE_PATH=$(pwd)/app/keystore.jks
echo -n "$KEYSTORE_BASE64" | base64 --decode > $KEYSTORE_PATH
./gradlew bundleRelease
- name: Run Test
- name: Run Unit Test
run: |
# TODO: See https://github.com/firezone/firezone/issues/2311
./gradlew testReleaseUnitTest
- name: Upload app bundle
uses: actions/upload-artifact@v4
Expand All @@ -78,3 +75,60 @@ jobs:
run: |
echo -n "$FIREBASE_APP_DISTRIBUTION_CREDENTIALS" > $FIREBASE_CREDENTIALS_PATH
./gradlew --info appDistributionUploadRelease uploadCrashlyticsSymbolFileRelease

ui-test:
name: ui-test-api-${{ matrix.api-level }}
Comment on lines +79 to +80
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it a ui-test because that has specific meaning in an android context. See: https://developer.android.com/training/testing/instrumented-tests/ui-tests

Other UI tests can base it off of this :)

strategy:
fail-fast: false
matrix:
include:
- api-level: 26
- api-level: 29
# Android SDK tools hardware accel is available only on Linux runners
runs-on: ubuntu-22.04
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
dotnet: true
haskell: true
large-packages: false
swap-storage: true
Comment on lines +90 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I run into issues with space on the runners

- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
targets: armv7-linux-androideabi aarch64-linux-android x86_64-linux-android i686-linux-android
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Run Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew --stacktrace connectedCheck
working-directory: ./kotlin/android
19 changes: 18 additions & 1 deletion kotlin/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ android {
// mark:automatic-version
versionName = "1.0.1"
multiDexEnabled = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner = "dev.firezone.android.core.HiltTestRunner"
}

signingConfigs {
Expand Down Expand Up @@ -146,6 +146,12 @@ android {
buildFeatures {
viewBinding = true
}

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

dependencies {
Expand All @@ -172,6 +178,16 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("androidx.hilt:hilt-compiler:1.2.0")
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
// Instrumented Tests
androidTestImplementation("com.google.dagger:hilt-android-testing:2.51")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.51.1")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.navigation:navigation-testing:2.7.7")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
// Unit Tests
testImplementation("com.google.dagger:hilt-android-testing:2.51")

// Retrofit 2
implementation("com.squareup.retrofit2:retrofit:2.11.0")
Expand All @@ -194,6 +210,7 @@ dependencies {
// JUnit
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.fragment:fragment-testing:1.6.2")

// Import the BoM for the Firebase platform
implementation(platform("com.google.firebase:firebase-bom:32.8.0"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
package dev.firezone.android.core

import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication

class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?,
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
package dev.firezone.android.core

import android.view.View
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import org.hamcrest.Matcher
import org.hamcrest.StringDescription
import java.util.concurrent.TimeoutException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

fun waitForView(
vararg matchers: Matcher<View>,
timeout: Duration = 5.seconds,
): ViewAction {
return object : ViewAction {
private val timeoutMillis = timeout.inWholeMilliseconds

override fun getConstraints() = isRoot()

override fun getDescription(): String {
val subDescription = StringDescription()
matchers.forEach { it.describeTo(subDescription) }
return "Wait for a view matching one of: $subDescription; with a timeout of $timeout."
}

override fun perform(
uiController: UiController,
rootView: View,
) {
uiController.loopMainThreadUntilIdle()
val startTime = System.currentTimeMillis()
val endTime = startTime + timeoutMillis

do {
for (child in TreeIterables.breadthFirstViewTraversal(rootView)) {
if (matchers.any { matcher -> matcher.matches(child) }) {
return
}
}
uiController.loopMainThreadForAtLeast(100)
} while (System.currentTimeMillis() < endTime)

throw PerformException.Builder()
.withCause(TimeoutException())
.withActionDescription(this.description)
.withViewDescription(HumanReadables.describe(rootView))
.build()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
package dev.firezone.android.core.di

import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import dev.firezone.android.core.ApplicationMode

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RuntimeModule::class],
)
object TestRuntimeModule {
@Provides
internal fun provideApplicationMode() = ApplicationMode.TESTING
}
Loading
Loading