测试中的 LiveData(由 Flow 支持)反映了旧值
LiveData (backed by Flow) in Test is reflecting old value
我正在尝试为我的视图模型编写一个测试来验证当我调用 setFirstTime
时,视图模型的 state
包含 firstTime
的更新值设置为 false
.
UserPreferencesRepository
向视图模型提供 Flow
首选项,将它们公开为 LiveData
(使用 asLiveData
扩展名)。
这是我遇到问题的测试:
MainViewModelTest.kt
package com.example.fitness.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.fitness.MainCoroutineRule
import com.example.fitness.data.UserPreferencesRepository
import com.example.fitness.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule
@ExperimentalCoroutinesApi
var mainCoroutineRule = MainCoroutineRule()
private lateinit var mainViewModel: MainViewModel
@Inject
lateinit var userPreferencesRepository: UserPreferencesRepository
@Before
@ExperimentalCoroutinesApi
fun init() {
hiltRule.inject()
// Execute all pending coroutine actions in MainViewModel initialization
mainCoroutineRule.runBlockingTest {
mainViewModel = MainViewModel(userPreferencesRepository)
}
}
@ExperimentalCoroutinesApi
@Test
fun `#setFirstTime marks the user as have opened the app at least once`() {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainCoroutineRule.runBlockingTest {
mainViewModel.setFirstTime()
}
# Failing assertion. Comes back as `true` when I expect it to be `false`
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
MainViewModel.kt
package com.example.fitness.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.example.fitness.data.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
val state = userPreferencesRepository.userPreferencesFlow.asLiveData()
/**
* Persists a value signifying that the user has started the app before.
*/
fun setFirstTime() {
viewModelScope.launch {
userPreferencesRepository.updateFirstTime(false)
}
}
}
UserPreferencesRepository
package com.example.fitness.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
data class UserPreferences(
val firstTime: Boolean
)
class UserPreferencesRepository @Inject constructor(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val FIRST_TIME = booleanPreferencesKey("first_time")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val firstTime = preferences[PreferencesKeys.FIRST_TIME] ?: true
UserPreferences(firstTime)
}
suspend fun updateFirstTime(firstTime: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.FIRST_TIME] = firstTime
}
}
}
我通过调试器验证了 dataStore.edit
代码的主体在测试的最后一个断言之前 运行。我还注意到 dataStore.data.map
的正文在更新后也是 运行,正确填充的 preferences
设置为 false。似乎 运行 在调试模式下测试并快速通过我的断点会导致测试通过,但 运行 测试通常会失败,这让我相信存在一些竞争条件存在。
我的工作基于 Google Codelab。任何帮助将不胜感激。
我设法确定了问题所在。当我在应用程序中创建我的 DataStore
时,我使用的是默认协程范围,即 Dispatchers.IO
。在我的测试中,我将主协程替换为 kotlinx.coroutines.test.TestCoroutineDispatcher
,但我还需要以某种方式将 DataStore
实例化为 TestCoroutineScope
,以便那些保存操作将 运行同步。
从这个 extremely helpful article 中获得了很多自由,我的最终代码如下所示:
MainViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class MainViewModelTest : DataStoreTest() {
private lateinit var mainViewModel: MainViewModel
@Before
fun init() = runBlockingTest {
val userPreferencesRepository = UserPreferencesRepository(dataStore)
mainViewModel = MainViewModel(userPreferencesRepository)
}
@Test
fun `#setFirstTime marks the user as having opened the app at least once`() = runBlockingTest {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainViewModel.setFirstTime()
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
DataStoreTest.kt
abstract class DataStoreTest : CoroutineTest() {
private lateinit var preferencesScope: CoroutineScope
protected lateinit var dataStore: DataStore<Preferences>
@Before
fun createDatastore() {
preferencesScope = CoroutineScope(testDispatcher + Job())
dataStore = PreferenceDataStoreFactory.create(scope = preferencesScope) {
InstrumentationRegistry.getInstrumentation().targetContext.preferencesDataStoreFile(
"test-preferences-file"
)
}
}
@After
fun removeDatastore() {
File(
ApplicationProvider.getApplicationContext<Context>().filesDir,
"datastore"
).deleteRecursively()
preferencesScope.cancel()
}
}
CoroutineTest.kt
abstract class CoroutineTest {
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
@Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
@After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
@After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}
我正在尝试为我的视图模型编写一个测试来验证当我调用 setFirstTime
时,视图模型的 state
包含 firstTime
的更新值设置为 false
.
UserPreferencesRepository
向视图模型提供 Flow
首选项,将它们公开为 LiveData
(使用 asLiveData
扩展名)。
这是我遇到问题的测试:
MainViewModelTest.kt
package com.example.fitness.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.fitness.MainCoroutineRule
import com.example.fitness.data.UserPreferencesRepository
import com.example.fitness.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule
@ExperimentalCoroutinesApi
var mainCoroutineRule = MainCoroutineRule()
private lateinit var mainViewModel: MainViewModel
@Inject
lateinit var userPreferencesRepository: UserPreferencesRepository
@Before
@ExperimentalCoroutinesApi
fun init() {
hiltRule.inject()
// Execute all pending coroutine actions in MainViewModel initialization
mainCoroutineRule.runBlockingTest {
mainViewModel = MainViewModel(userPreferencesRepository)
}
}
@ExperimentalCoroutinesApi
@Test
fun `#setFirstTime marks the user as have opened the app at least once`() {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainCoroutineRule.runBlockingTest {
mainViewModel.setFirstTime()
}
# Failing assertion. Comes back as `true` when I expect it to be `false`
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
MainViewModel.kt
package com.example.fitness.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.example.fitness.data.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
val state = userPreferencesRepository.userPreferencesFlow.asLiveData()
/**
* Persists a value signifying that the user has started the app before.
*/
fun setFirstTime() {
viewModelScope.launch {
userPreferencesRepository.updateFirstTime(false)
}
}
}
UserPreferencesRepository
package com.example.fitness.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
data class UserPreferences(
val firstTime: Boolean
)
class UserPreferencesRepository @Inject constructor(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val FIRST_TIME = booleanPreferencesKey("first_time")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val firstTime = preferences[PreferencesKeys.FIRST_TIME] ?: true
UserPreferences(firstTime)
}
suspend fun updateFirstTime(firstTime: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.FIRST_TIME] = firstTime
}
}
}
我通过调试器验证了 dataStore.edit
代码的主体在测试的最后一个断言之前 运行。我还注意到 dataStore.data.map
的正文在更新后也是 运行,正确填充的 preferences
设置为 false。似乎 运行 在调试模式下测试并快速通过我的断点会导致测试通过,但 运行 测试通常会失败,这让我相信存在一些竞争条件存在。
我的工作基于 Google Codelab。任何帮助将不胜感激。
我设法确定了问题所在。当我在应用程序中创建我的 DataStore
时,我使用的是默认协程范围,即 Dispatchers.IO
。在我的测试中,我将主协程替换为 kotlinx.coroutines.test.TestCoroutineDispatcher
,但我还需要以某种方式将 DataStore
实例化为 TestCoroutineScope
,以便那些保存操作将 运行同步。
从这个 extremely helpful article 中获得了很多自由,我的最终代码如下所示:
MainViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class MainViewModelTest : DataStoreTest() {
private lateinit var mainViewModel: MainViewModel
@Before
fun init() = runBlockingTest {
val userPreferencesRepository = UserPreferencesRepository(dataStore)
mainViewModel = MainViewModel(userPreferencesRepository)
}
@Test
fun `#setFirstTime marks the user as having opened the app at least once`() = runBlockingTest {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainViewModel.setFirstTime()
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
DataStoreTest.kt
abstract class DataStoreTest : CoroutineTest() {
private lateinit var preferencesScope: CoroutineScope
protected lateinit var dataStore: DataStore<Preferences>
@Before
fun createDatastore() {
preferencesScope = CoroutineScope(testDispatcher + Job())
dataStore = PreferenceDataStoreFactory.create(scope = preferencesScope) {
InstrumentationRegistry.getInstrumentation().targetContext.preferencesDataStoreFile(
"test-preferences-file"
)
}
}
@After
fun removeDatastore() {
File(
ApplicationProvider.getApplicationContext<Context>().filesDir,
"datastore"
).deleteRecursively()
preferencesScope.cancel()
}
}
CoroutineTest.kt
abstract class CoroutineTest {
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
@Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
@After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
@After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}