Kotlin MVVM структурированное приложение ViewModel тестирует ошибку и иногда завершается неудачно с Wanted, но не вызывается, с этим макетом не было никаких взаимодействий - PullRequest
0 голосов
/ 17 марта 2020

Я пытаюсь написать тест для моей 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)
    }

}   
...