Skip to content

Commit

Permalink
test(android): add ui tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonboukheir committed Apr 24, 2024
1 parent 0b83b12 commit a8e87a9
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 10 deletions.
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 }}
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
- 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

0 comments on commit a8e87a9

Please sign in to comment.