Я пытаюсь написать тест для моей ViewModel в моем приложении Kotlin, которое использует структуру MVVM.
Когда я запускаю свой тест, иногда он проходит, а иногда не проходит со следующей ошибкой. (обратите внимание, что я заменил фактические значения и путь к пакету на ..... для краткости)
Wanted but not invoked:
observer.onChanged(
MyDataObject(.......)
);
-> at ......MyViewModelTest.testOne(MyViewModelTest.kt:107)
Actually, there were zero interactions with this mock.
Нет изменений кода между прохождением теста и его провалом, поэтому я не могу понять, что я кодировал неправильно.
Мой код объяснил
- У меня есть данные, хранящиеся в базе данных (с использованием ROOM).
- У меня есть 2 предпочтения, которые используются для определения, какие строки получить из базы данных.
- My ViewModel получает данные (карту) из базы данных, а затем создает объект данных, содержащий карту затем он устанавливает для элемента LiveData этот dataObject
- Мой тест проверяет настройки так, чтобы хранилище реальных предпочтений не использовалось
- Мой тест проверяет хранилище, поэтому база данных ROOM фактически никогда не вызывается
- Мой тест просто вызывает метод publi c в модели представления, которая получает данные из БД и устанавливает элемент данных в реальном времени, затем наблюдателю следует сообщить, что данные изменились
ViewModel
class MyViewModel(private val repos: MyRepository) : ViewModel() {
var map : MutableMap<String, String> = HashMap()
var myDataObject = MutableLiveData<MyDataObject>()
fun initMyDataObject(context: Context){
// use a coroutine to get the data from the database
viewModelScope.launch(Dispatchers.IO) {
map = getMapInternal(context)
// create a new MyDataObject with the map obtained from the database
var data = MyDataObject(map)
// post value back to any observers on main UI
myDataObject.postValue(data)
}
}
private suspend fun getMapInternal(context: Context) : MutableMap<String, String> {
// get the settings from the pref store
val pref1value = SettingsUtils.getPref1value(context)
val pref2value = SettingsUtils.getPref2value(context)
// get the data from the database using the Repository
return repos.getMap(pref1value, pref2value)
}
}
Тестовый класс
@RunWith(RobolectricTestRunner::class)
class ViewModelTest {
// this rule is required to ensure Architecture Components-related background jobs in the
// same thread so that the test results happen synchronously, and in a repeatable order.
// This is needed when you have tests that include testing LiveData
@Rule @JvmField
val instantExecutorRule = InstantTaskExecutorRule()
// ---- Mocked objects ----
@Mock
var repos: MyRepository? = null
@Mock
var observer: Observer<MyDataObject<String>>? = null
@Mock
var mockPrefs : SharedPreferences? = null
@Mock
var mockContext: Context? = null
// ---- View model under test ----
private var viewModel: MyViewModel? = null
// Expected data
private val expectedValues: MutableMap<String, String> =
mutableMapOf("a" to "Apple", "b" to "Banana.png")
private val expectedMyDataObject: MyDataObject = MyDataObject(expectedValues1)
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
// When "getSharedPreferences" is called, return the mocked preferences
`when`(mockContext?.getSharedPreferences(anyString(), anyInt())).thenReturn(mockPrefs)
// Create the ViewModel to test passing in the MOCKED repository
viewModel = MyViewModel(repos!!)
// Add the observer on the MyViewModel's "myDataObject" member
viewModel!!.myDataObject.observeForever(observer!!)
}
@After
@Throws(Exception::class)
fun tearDown() {
repos = null
viewModel = null
}
/*
* Test that when "MyViewModel.initMyDataObject" is called, the
* observer is fired returning a MyDataObject object with the correct values.
*/
@Test
fun testOne() {
`when`(mockPrefs!!.getBoolean(eq(SettingsUtils.PREF_1), anyBoolean())).thenReturn(true)
`when`(mockPrefs!!.getBoolean(eq(SettingsUtils.PREF_2), anyBoolean())).thenReturn(true)
// Mock API response on the repos object :
// When "getMap" is called on the mocked repos, return the expectedMyDataObject object
// defined at the top of this test class
runBlocking {
`when`(repos!!.getMap(1,1)).thenReturn(expectedMyDataObject)
}
// Call the API on the ACTUAL View Model (passing in the mocked context)
viewModel!!.initMyDataObject(mockContext!!)
// Verify the observer is fired
verify(observer)!!.onChanged(expectedMyDataObject)
}
}