Skip to content

Commit

Permalink
Add example test for MainViewModel (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck authored Jun 3, 2024
1 parent c819454 commit d1b5f30
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 3 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
60 changes: 60 additions & 0 deletions app/src/test/java/com/bitwarden/authenticator/MainViewModelTest.kt
Original file line number Diff line number Diff line change
@@ -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<SettingsRepository> {
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <S : Any, E : Any, T : BaseViewModel<S, E, *>> T.stateEventFlow(
backgroundScope: CoroutineScope,
validate: suspend TurbineContext.(
stateFlow: ReceiveTurbine<S>,
eventFlow: ReceiveTurbine<E>,
) -> Unit,
) {
turbineScope {
this.validate(
this@stateEventFlow.stateFlow.testIn(backgroundScope),
this@stateEventFlow.eventFlow.testIn(backgroundScope),
)
}
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit d1b5f30

Please sign in to comment.