Skip to content

Commit

Permalink
Unit test the SleepTimerDialogViewModel.
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulWoitaschek committed Jul 4, 2022
1 parent 7ec8af6 commit ebbe616
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 5 deletions.
8 changes: 8 additions & 0 deletions app/src/main/kotlin/voice/app/injection/AndroidModule.kt
Expand Up @@ -12,11 +12,13 @@ import android.view.WindowManager
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json
import voice.app.misc.ApplicationIdProviderImpl
import voice.app.misc.ToBookIntentProviderImpl
import voice.common.AppScope
import voice.common.ApplicationIdProvider
import voice.common.DispatcherProvider
import voice.playback.notification.ToBookIntentProvider
import javax.inject.Singleton

Expand Down Expand Up @@ -82,4 +84,10 @@ object AndroidModule {
fun json(): Json {
return Json.Default
}

@Provides
@Singleton
fun dispatcherProvider(): DispatcherProvider {
return DispatcherProvider(Dispatchers.Main)
}
}
7 changes: 7 additions & 0 deletions common/src/main/kotlin/voice/common/DispatcherProvider.kt
@@ -0,0 +1,7 @@
package voice.common

import kotlin.coroutines.CoroutineContext

data class DispatcherProvider(
val main: CoroutineContext
)
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Expand Up @@ -47,6 +47,7 @@ datastore = "androidx.datastore:datastore-preferences:1.0.0"
seismic = "com.squareup:seismic:1.0.3"
viewBinding = { module = "androidx.databinding:viewbinding", version.ref = "agp" }
appStartup = "androidx.startup:startup-runtime:1.1.1"
turbine = "app.cash.turbine:turbine:0.8.0"

coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
Expand Down
10 changes: 10 additions & 0 deletions plugins/src/main/kotlin/baseSetup.kt
Expand Up @@ -65,5 +65,15 @@ fun Project.baseSetup() {
if (project.path != ":logging:core") {
add("implementation", project(":logging:core"))
}

listOf(
"junit",
"koTest.assert",
"mockk",
"turbine",
"coroutines.test"
).forEach {
add("testImplementation", libs.findLibrary(it).get())
}
}
}
2 changes: 2 additions & 0 deletions sleepTimer/build.gradle.kts
Expand Up @@ -29,4 +29,6 @@ dependencies {
implementation(libs.seismic)

implementation(libs.dagger.core)

testImplementation(libs.prefs.inMemory)
}
@@ -1,12 +1,14 @@
package voice.sleepTimer

import de.paulwoitaschek.flowpref.Pref
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import voice.common.DispatcherProvider
import voice.common.pref.PrefKeys
import voice.data.Book
import voice.data.repo.BookRepository
Expand All @@ -20,10 +22,11 @@ class SleepTimerDialogViewModel
private val sleepTimer: SleepTimer,
private val bookRepo: BookRepository,
@Named(PrefKeys.SLEEP_TIME)
private val sleepTimePref: Pref<Int>
private val sleepTimePref: Pref<Int>,
dispatcherProvider: DispatcherProvider,
) {

private val scope = MainScope()
private val scope = CoroutineScope(dispatcherProvider.main + SupervisorJob())

private val selectedMinutes = MutableStateFlow(sleepTimePref.value)

Expand All @@ -44,7 +47,6 @@ class SleepTimerDialogViewModel
if (newValue > 999) {
oldValue
} else {
sleepTimePref.value = newValue
newValue
}
}
Expand All @@ -60,7 +62,7 @@ class SleepTimerDialogViewModel

fun onConfirmButtonClicked(bookId: Book.Id) {
check(selectedMinutes.value > 0)

sleepTimePref.value = selectedMinutes.value
scope.launch {
val book = bookRepo.get(bookId) ?: return@launch
bookmarkRepo.addBookmarkAtBookPosition(
Expand Down
@@ -0,0 +1,149 @@
package voice.sleepTimer

import app.cash.turbine.test
import de.paulwoitaschek.flowpref.inmemory.InMemoryPref
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.shouldBe
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import voice.common.DispatcherProvider
import voice.data.Book
import voice.data.BookContent
import voice.data.Chapter
import voice.data.repo.BookRepository
import voice.data.repo.BookmarkRepo
import java.time.Instant
import java.util.UUID
import kotlin.time.Duration.Companion.minutes

class SleepTimerDialogViewModelTest {

private val book = book()
private val sleepTimePref = InMemoryPref(42)
private val scope = TestScope()
private val sleepTimer = mockk<SleepTimer>().also {
every { it.setActive(any()) } just Runs
}
private val bookRepo = mockk<BookRepository>().also {
coEvery { it.get(book.id) } returns book
}
private val bookmarkRepo = mockk<BookmarkRepo>().also {
coEvery {
it.addBookmarkAtBookPosition(
book = book,
title = null,
setBySleepTimer = true
)
} returns mockk()
}
private val viewModel = SleepTimerDialogViewModel(
bookmarkRepo = bookmarkRepo,
sleepTimer = sleepTimer,
bookRepo = bookRepo,
sleepTimePref = sleepTimePref,
dispatcherProvider = DispatcherProvider(scope.coroutineContext)
)

@Test
fun `sleep time pref used as default value`() = test {
viewModel.viewState().test {
awaitItem().selectedMinutes shouldBeExactly 42
}
}

@Test
fun `sleep time pref updated when adding`() = test {
viewModel.onNumberClicked(1)
sleepTimePref.value shouldBe 42
viewModel.onConfirmButtonClicked(book.id)
sleepTimePref.value shouldBe 421
}

@Test
fun `changing values works`() = test {
viewModel.viewState().test {
suspend fun expect(selectedMinutes: Int, showFab: Boolean = true) {
awaitItem() shouldBe SleepTimerDialogViewState(
selectedMinutes = selectedMinutes,
showFab = showFab
)
}
expect(42)
viewModel.onNumberClicked(4)
expect(424)
viewModel.onNumberClicked(4)
expectNoEvents()
viewModel.onNumberDeleteClicked()
expect(42)
viewModel.onNumberDeleteClicked()
expect(4)
viewModel.onNumberDeleteClicked()
expect(0, showFab = false)
viewModel.onNumberClicked(1)
expect(1)
viewModel.onNumberClicked(2)
expect(12)
viewModel.onNumberDeleteLongClicked()
expect(0, showFab = false)
}
}

@Test
fun `bookmark is added`() = test {
viewModel.onConfirmButtonClicked(book.id)
runCurrent()
coVerify(exactly = 1) {
sleepTimer.setActive(true)
bookmarkRepo.addBookmarkAtBookPosition(book, title = null, setBySleepTimer = true)
}
}

private fun test(testBody: suspend TestScope.() -> Unit) {
scope.runTest(testBody = testBody)
}
}

private fun book(
name: String = "TestBook",
lastPlayedAtMillis: Long = 0L,
addedAtMillis: Long = 0L
): Book {
val chapters = listOf(
chapter(), chapter(),
)
return Book(
content = BookContent(
author = UUID.randomUUID().toString(),
name = name,
positionInChapter = 42,
playbackSpeed = 1F,
addedAt = Instant.ofEpochMilli(addedAtMillis),
chapters = chapters.map { it.id },
cover = null,
currentChapter = chapters.first().id,
isActive = true,
lastPlayedAt = Instant.ofEpochMilli(lastPlayedAtMillis),
skipSilence = false,
id = Book.Id(UUID.randomUUID().toString())
),
chapters = chapters,
)
}

private fun chapter(): Chapter {
return Chapter(
id = Chapter.Id("http://${UUID.randomUUID()}"),
duration = 5.minutes.inWholeMilliseconds,
fileLastModified = Instant.EPOCH,
markData = emptyList(),
name = "name"
)
}

0 comments on commit ebbe616

Please sign in to comment.