Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ private val Context.settingsDataStore: DataStore<Preferences> by preferencesData
name = "app_settings",
)

class SettingsRepository(context: Context) {
private val dataStore = context.applicationContext.settingsDataStore
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
constructor(context: Context) : this(context.applicationContext.settingsDataStore)

val settings: Flow<AppSettings> = dataStore.data.map { prefs ->
AppSettings(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.clhs.score.data

import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import java.io.File
import kotlinx.coroutines.flow.first
Comment on lines +3 to +5
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import app.cash.turbine.test
import kotlinx.coroutines.ExperimentalCoroutinesApi

@OptIn(ExperimentalCoroutinesApi::class)
class SettingsRepositoryTest {
@get:Rule
val tempFolder = TemporaryFolder()

private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)

private fun createRepository(): SettingsRepository {
val dataStore = PreferenceDataStoreFactory.create(
scope = testScope,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use backgroundScope for test DataStore jobs

When these tests collect repository.settings, DataStore 1.1+ starts long-lived work in the scope passed to PreferenceDataStoreFactory.create; because that scope is the same TestScope that runs each runTest and is never cancelled as background work, runTest will wait for the active DataStore job and fail or time out with UncompletedCoroutinesError. Pass the current test's backgroundScope into the factory (or cancel a separate DataStore scope in teardown) so the DataStore job is cleaned up after each test.

Useful? React with 👍 / 👎.

produceFile = { tempFolder.newFile("test_settings.preferences_pb") }
)
return SettingsRepository(dataStore)
}
Comment on lines +23 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Sharing a single TestScope instance across multiple tests via a class property is an anti-pattern in kotlinx.coroutines.test. When a test finishes, the TestScope is completed/cancelled, which will cause subsequent tests using the same scope to fail or behave unpredictably.

Instead, each test should run in its own isolated TestScope by calling runTest(testDispatcher). We can refactor createRepository to be an extension function on TestScope so it can use the current test's scope.

Additionally, calling tempFolder.newFile(...) inside the produceFile lambda of PreferenceDataStoreFactory.create can throw an IOException if the file already exists or is initialized multiple times. It is safer to resolve the file path using File(tempFolder.root, ...) instead.

    private val testDispatcher = UnconfinedTestDispatcher()

    private fun TestScope.createRepository(): SettingsRepository {
        val dataStore = PreferenceDataStoreFactory.create(
            scope = this,
            produceFile = { File(tempFolder.root, "test_settings.preferences_pb") }
        )
        return SettingsRepository(dataStore)
    }


@Test
fun testDefaultSettings() = testScope.runTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use runTest(testDispatcher) instead of the shared testScope.runTest to ensure each test runs in its own isolated TestScope.

Suggested change
fun testDefaultSettings() = testScope.runTest {
fun testDefaultSettings() = runTest(testDispatcher) {

val repository = createRepository()

val initialSettings = repository.settings.first()
assertEquals(ThemeMode.SYSTEM, initialSettings.themeMode)
assertFalse(initialSettings.dynamicColor)
assertFalse(initialSettings.amoledBlack)
assertFalse(initialSettings.notificationsEnabled)
assertFalse(initialSettings.notificationPromptDismissed)
assertFalse(initialSettings.developerEnabled)
assertFalse(initialSettings.demoMode)
assertFalse(initialSettings.biometricEnabled)
}


@Test
fun testUpdateSettings() = testScope.runTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use runTest(testDispatcher) instead of the shared testScope.runTest to ensure each test runs in its own isolated TestScope.

Suggested change
fun testUpdateSettings() = testScope.runTest {
fun testUpdateSettings() = runTest(testDispatcher) {

val repository = createRepository()

repository.setThemeMode(ThemeMode.DARK)
assertEquals(ThemeMode.DARK, repository.settings.first().themeMode)

repository.setDynamicColor(true)
assertTrue(repository.settings.first().dynamicColor)

repository.setAmoledBlack(true)
assertTrue(repository.settings.first().amoledBlack)

repository.setNotificationsEnabled(true)
assertTrue(repository.settings.first().notificationsEnabled)

repository.setNotificationPromptDismissed(true)
assertTrue(repository.settings.first().notificationPromptDismissed)

repository.setDeveloperEnabled(true)
assertTrue(repository.settings.first().developerEnabled)

repository.setDemoMode(true)
assertTrue(repository.settings.first().demoMode)

repository.setBiometricEnabled(true)
assertTrue(repository.settings.first().biometricEnabled)
}

@Test
fun testMultipleUpdates() = testScope.runTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Use runTest(testDispatcher) instead of the shared testScope.runTest to ensure each test runs in its own isolated TestScope.

Suggested change
fun testMultipleUpdates() = testScope.runTest {
fun testMultipleUpdates() = runTest(testDispatcher) {

val repository = createRepository()

repository.settings.test {
// Initial state
val initial = awaitItem()
assertEquals(ThemeMode.SYSTEM, initial.themeMode)
assertFalse(initial.demoMode)

// Update theme
repository.setThemeMode(ThemeMode.LIGHT)
val updatedTheme = awaitItem()
assertEquals(ThemeMode.LIGHT, updatedTheme.themeMode)
assertFalse(updatedTheme.demoMode)

// Update demo mode
repository.setDemoMode(true)
val updatedDemo = awaitItem()
assertEquals(ThemeMode.LIGHT, updatedDemo.themeMode)
assertTrue(updatedDemo.demoMode)

cancelAndIgnoreRemainingEvents()
}
}
}
Loading