From d1b5f3078e2bdd857f355da06db2a187c9c88906 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:43:13 -0400 Subject: [PATCH] Add example test for MainViewModel (#113) --- .github/workflows/test.yml | 13 +++- app/build.gradle.kts | 55 +++++++++++++++++ .../authenticator/MainViewModelTest.kt | 60 +++++++++++++++++++ .../ui/platform/base/BaseViewModelTest.kt | 29 +++++++++ .../platform/base/MainDispatcherExtension.kt | 45 ++++++++++++++ fastlane/Fastfile | 2 +- gradle/libs.versions.toml | 2 +- 7 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 app/src/test/java/com/bitwarden/authenticator/MainViewModelTest.kt create mode 100644 app/src/test/java/com/bitwarden/authenticator/ui/platform/base/BaseViewModelTest.kt create mode 100644 app/src/test/java/com/bitwarden/authenticator/ui/platform/base/MainDispatcherExtension.kt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52c0d9211..44a25985c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,15 +55,26 @@ jobs: restore-keys: | ${{ runner.os }}-build- + - name: Configure Ruby + uses: ruby/setup-ruby@0cde4689ba33c09f1b890c1725572ad96751a3fc # v1.178.0 + with: + bundler-cache: true + - name: Configure JDK uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 with: distribution: "temurin" java-version: ${{ env.JAVA_VERSION }} + - name: Install Fastlane + run: | + gem install bundler:2.2.27 + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Build and test run: | - ./gradlew testDebug lintDebug + bundle exec fastlane check - name: Upload to codecov.io uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2abc233bc..1ec4da9bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,61 @@ dependencies { androidTestImplementation(libs.bundles.tests.instrumented) } +kover { + currentProject { + sources { + excludeJava = true + } + } + reports { + filters { + excludes { + androidGeneratedClasses() + annotatedBy( + // Compose previews + "androidx.compose.ui.tooling.preview.Preview", + // Manually excluded classes/files/etc. + "com.bitwarden.authenticator.data.platform.annotation.OmitFromCoverage", + ) + classes( + // Navigation helpers + "*.*NavigationKt*", + // Composable singletons + "*.*ComposableSingletons*", + // Generated classes related to interfaces with default values + "*.*DefaultImpls*", + // Databases + "*.database.*Database*", + "*.dao.*Dao*", + // Dagger Hilt + "dagger.hilt.*", + "hilt_aggregated_deps.*", + "*_Factory", + "*_Factory\$*", + "*_*Factory", + "*_*Factory\$*", + "*.Hilt_*", + "*_HiltModules", + "*_HiltModules\$*", + "*_Impl", + "*_Impl\$*", + "*_MembersInjector", + ) + packages( + // Dependency injection + "*.di", + // Models + "*.model", + // Custom UI components + "com.bitwarden.authenticator.ui.platform.components", + // Theme-related code + "com.bitwarden.authenticator.ui.platform.theme", + ) + } + } + } +} + protobuf { protoc { artifact = libs.google.protobuf.protoc.get().toString() diff --git a/app/src/test/java/com/bitwarden/authenticator/MainViewModelTest.kt b/app/src/test/java/com/bitwarden/authenticator/MainViewModelTest.kt new file mode 100644 index 000000000..1d8c7043b --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/MainViewModelTest.kt @@ -0,0 +1,60 @@ +package com.bitwarden.authenticator + +import com.bitwarden.authenticator.data.platform.repository.SettingsRepository +import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest +import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MainViewModelTest : BaseViewModelTest() { + + private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT) + private val mutableScreenCaptureAllowedFlow = MutableStateFlow(false) + private val settingsRepository = mockk { + every { appTheme } returns AppTheme.DEFAULT + every { appThemeStateFlow } returns mutableAppThemeFlow + every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow + } + private lateinit var mainViewModel: MainViewModel + + @BeforeEach + fun setUp() { + mainViewModel = MainViewModel(settingsRepository) + } + + @AfterEach + fun tearDown() { + } + + @Test + fun `on AppThemeChanged should update state`() { + assertEquals( + MainState( + theme = AppTheme.DEFAULT, + ), + mainViewModel.stateFlow.value, + ) + mainViewModel.trySendAction( + MainAction.Internal.ThemeUpdate( + theme = AppTheme.DARK, + ), + ) + assertEquals( + MainState( + theme = AppTheme.DARK, + ), + mainViewModel.stateFlow.value, + ) + + verify { + settingsRepository.appTheme + settingsRepository.appThemeStateFlow + } + } +} diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/BaseViewModelTest.kt b/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/BaseViewModelTest.kt new file mode 100644 index 000000000..eabadd1a5 --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/BaseViewModelTest.kt @@ -0,0 +1,29 @@ +package com.bitwarden.authenticator.ui.platform.base + +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.TurbineContext +import app.cash.turbine.turbineScope +import kotlinx.coroutines.CoroutineScope +import org.junit.jupiter.api.extension.RegisterExtension + +abstract class BaseViewModelTest { + @Suppress("unused", "JUnitMalformedDeclaration") + @RegisterExtension + protected open val mainDispatcherExtension = MainDispatcherExtension() + + protected suspend fun > T.stateEventFlow( + backgroundScope: CoroutineScope, + validate: suspend TurbineContext.( + stateFlow: ReceiveTurbine, + eventFlow: ReceiveTurbine, + ) -> Unit, + ) { + turbineScope { + this.validate( + this@stateEventFlow.stateFlow.testIn(backgroundScope), + this@stateEventFlow.eventFlow.testIn(backgroundScope), + ) + } + } +} + diff --git a/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/MainDispatcherExtension.kt b/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/MainDispatcherExtension.kt new file mode 100644 index 000000000..2dfb01508 --- /dev/null +++ b/app/src/test/java/com/bitwarden/authenticator/ui/platform/base/MainDispatcherExtension.kt @@ -0,0 +1,45 @@ +package com.bitwarden.authenticator.ui.platform.base + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.RegisterExtension + +/** + * JUnit 5 Extension for automatically setting a [testDispatcher] as the "main" dispatcher. + * + * Note that this may be used as a normal class property with [RegisterExtension] or may be applied + * directly to a test class using [ExtendWith]. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class MainDispatcherExtension( + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : AfterAllCallback, + AfterEachCallback, + BeforeAllCallback, + BeforeEachCallback { + override fun afterAll(context: ExtensionContext?) { + Dispatchers.resetMain() + } + + override fun afterEach(context: ExtensionContext?) { + Dispatchers.resetMain() + } + + override fun beforeAll(context: ExtensionContext?) { + Dispatchers.setMain(testDispatcher) + } + + override fun beforeEach(context: ExtensionContext?) { + Dispatchers.setMain(testDispatcher) + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 460390cc6..49f7eed0d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :android do desc "Runs tests" lane :check do - gradle(task: "check") + gradle(tasks: ["check","koverXmlReportDebug"]) end desc "Apply build version information" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0dcd8d65f..b60368930 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,7 +47,7 @@ kotlinCompilerExtensionVersion = "1.5.14" kotlinxCollectionsImmutable = "0.3.7" kotlinxCoroutines = "1.8.1" kotlinxSerialization = "1.6.3" -kotlinxKover = "0.7.6" +kotlinxKover = "0.8.0" ksp = "1.9.24-1.0.20" mockk = "1.13.10" okhttp = "4.12.0"