web-dev-qa-db-fra.com

Comment se moquer correctement de ViewModel sur androidTest

Je suis en train d'écrire des tests unitaires d'interface utilisateur pour un fragment, et l'un de ces @Test est de voir si une liste d'objets est correctement affichée, c'est pas un test d'intégration, donc je souhaite mock la ViewModel.

Les fragments du fragment:

class FavoritesFragment : Fragment() {

    private lateinit var adapter: FavoritesAdapter
    private lateinit var viewModel: FavoritesViewModel
    @Inject lateinit var viewModelFactory: FavoritesViewModelFactory

    (...)

Voici le code:

@MediumTest
@RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {

    @Rule @JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.Java, true, true)
    @Rule @JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()

    private val results = MutableLiveData<Resource<List<FavoriteView>>>()
    private val viewModel = mock(FavoritesViewModel::class.Java)

    private lateinit var favoritesFragment: FavoritesFragment

    @Before
    fun setup() {
        favoritesFragment = FavoritesFragment.newInstance()
        activityRule.activity.addFragment(favoritesFragment)
        `when`(viewModel.getFavourites()).thenReturn(results)
    }

    (...)

    // This is the initial part of the test where I intend to Push to the view
    @Test
    fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
        val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
        results.postValue(Resource.success(resultsList))

        (...)
    }

J'ai pu me moquer de la ViewModel mais bien sûr, ce n'est pas la même ViewModel créée à l'intérieur de la Fragment.

Ma question est donc la suivante: quelqu'un a-t-il réussi à le faire ou a-t-il des indications/références qui pourraient m'aider?

5
Joaquim Ley

Dans l'exemple que vous avez fourni, vous utilisez mockito pour renvoyer une maquette pour une instance spécifique de votre modèle de vue, et non pour toutes les instances.

Pour que cela fonctionne, votre fragment devra utiliser la maquette de modèle de vue exacte que vous avez créée.

Cela viendrait très probablement d'un magasin ou d'un référentiel, de sorte que vous puissiez y déposer votre maquette? Cela dépend vraiment de la manière dont vous configurez l'acquisition du modèle de vue dans votre logique Fragments.

Recommandations: 1) Simulez les sources de données à partir desquelles le modèle de vue est construit ou 2) ajoutez un fragment.setViewModel () et marquez-le uniquement à des fins de test. C’est un peu moche, mais si vous ne voulez pas vous moquer des sources de données, c’est assez facile de cette façon.

1
Sam Edwards

Dans votre configuration de test, vous devrez fournir une version test de FavoritesViewModelFactory qui est injectée dans le fragment.

Vous pouvez faire quelque chose comme ceci, où le module devra être ajouté à votre TestAppComponent:

@Module
object TestFavoritesViewModelModule {

    val viewModelFactory: FavoritesViewModelFactory = mock()

    @JvmStatic
    @Provides
    fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
        return viewModelFactory
    }
}

Vous seriez alors en mesure de fournir votre Mock viewModel au test.

fun setupViewModelFactory() {
    whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.Java)).thenReturn(viewModel)
}
4
Chris

On dirait que vous utilisez kotlin et koin (1.0-beta) . C'est ma décision de me moquer

@RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.Java, true, true)
@Rule
@JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
@Rule
@JvmField
val countingAppExecutors = CountingAppExecutorsRule()

private val testFragment = DashboardFragment()

private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router

private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()

@Before
fun setUp() {
    dashboardViewModel = Mockito.mock(DashboardViewModel::class.Java)
    Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
    Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
    Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }

    router = Mockito.mock(Router::class.Java)
    Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }

    StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
        single(override = true) { router }
        factory(override = true) { dashboardViewModel } bind ViewModel::class
    }))

    activityRule.activity.setFragment(testFragment)
    EspressoTestUtil.disableProgressBarAnimations(activityRule)
}

@After
fun tearDown() {
    activityRule.finishActivity()
    StandAloneContext.closeKoin()
}

@Test
fun devicesSuccess(){
    val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
    devicesSuccess.postValue(list)
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
    onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}

@Test
fun devicesFailure(){
    devicesFailure.postValue("error")
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}

@Test
fun devicesCall() {
    onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
    Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}

}

3
Aleksandr Rusin

J'ai résolu ce problème en utilisant un objet supplémentaire injecté par Dagger, vous pouvez trouver l'exemple complet ici: https://github.com/fabioCollini/ArchitectureComponentsDemo

Dans le fragment que je n'utilise pas directement ViewModelFactory, j'ai défini une fabrique personnalisée définie comme un singleton de poignard: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/ Java/it/codingjam/github/ui/search/SearchFragment.kt

Ensuite, dans le test, je remplace par DaggerMock cette fabrique personnalisée à l'aide d'une fabrique qui retourne toujours une maquette au lieu de la vue réelle Modèle: https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/ uisearchTest/src/androidTest/Java/it/codingjam/github/ui/repo/SearchFragmentTest.kt

2
Fabio Collini

On pourrait facilement se moquer d'un ViewModel et d'autres objets sans Dagger simplement en:

  1. Créez une classe wrapper pouvant rediriger les appels vers ViewModelProvider. Vous trouverez ci-dessous la version de production de la classe wrapper qui transmet simplement les appels au véritable ViewModelProvider qui est transmis en tant que paramètre.

    class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
        return viewModelProvider.get(x)
    }
    

    }

  2. Ajout de getters et setters pour cet objet wrapper à la classe Application.

  3. Dans la règle d'activité, avant le lancement d'une activité, remplacez le wrapper réel par un wrapper simulé ne routant pas l'appel get ViewModel vers le vrai viewModelProvider et fournissant un objet simulé.

Je réalise que ce n’est pas aussi puissant que le poignard mais la simplicité est séduisante.

0
A.Sanchez.SD