9
votes

Comment s'assurer que ViewModel # onCleared est appelé dans un test unitaire Android?

Voici ma classe de test MWE, qui dépend d'AndroidX, JUnit 4 et MockK 1.9:

class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        MyViewModel::class.members
            .single { it.name == "onCleared" }
            .apply { isAccessible = true }
            .call(MyViewModel())

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

Remarque: la méthode est protégée dans la superclasse ViewModel .

/ p>

Je veux vérifier que MyViewModel # onCleared appelle Object # function . Le code ci-dessus a accompli cela par réflexion. Ma question est la suivante: puis-je exécuter ou me moquer du système Android pour que la méthode onCleared soit appelée, de sorte que je n'ai pas besoin de réflexion?

À partir de onCleared JavaDoc:

Cette méthode sera appelée lorsque ce ViewModel n'est plus utilisé et sera détruit.

Alors, en d'autres termes, comment créer cette situation pour que je sache que onCleared est appelé et que je puisse vérifier son comportement?


1 commentaires

Vous pouvez public override fun onCleared () , mais cela expose la méthode qui n'est pas bonne, car la méthode ne doit être appelée que par le système Android.


3 Réponses :


5
votes

TL; DR

Dans cette réponse, Robolectric est utilisé pour que le framework Android appelle onCleared sur votre ViewModel . Cette méthode de test est plus lente que l'utilisation de la réflexion (comme dans la question) et dépend à la fois de Robolectric et du framework Android. Ce compromis dépend de vous.


En regardant la source d'Android ...

... vous pouvez voir que ViewModel # onCleared n'est appelé que dans ViewModelStore (pour vos propres ViewModels ). Il s'agit d'une classe de stockage pour les modèles de vue et appartient aux classes ViewModelStoreOwner , par exemple FragmentActivity . Alors, quand est-ce que ViewModelStore appelle onCleared sur votre ViewModel?

Il doit stocker votre ViewModel code>, alors le magasin doit être effacé (ce que vous ne pouvez pas faire vous-même).

Votre modèle de vue est stocké par le ViewModelProvider lorsque vous obtenez votre ViewModel en utilisant ViewModelProviders.of (FragmentActivity activity) .get (Class modelClass) , où T est votre classe de modèle de vue. Il le stocke dans le ViewModelStore du FragmentActivity.

Le magasin est vide par exemple lorsque votre activité de fragment est détruite. C'est un tas d'appels enchaînés qui vont partout, mais en gros c'est:

  1. Avoir une FragmentActivity .
  2. Obtenez son ViewModelProvider en utilisant ViewModelProviders # of .
  3. Obtenez votre ViewModel à l'aide de ViewModelProvider # get .
  4. Détruisez votre activité.

Maintenant, onCleared devrait être appelé sur votre modèle de vue. Testons-le en utilisant Robolectric 4, JUnit 4, MockK 1.9:

  1. Ajoutez @RunWith (RobolectricTestRunner :: class) à votre classe de test.
  2. Créez un contrôleur d'activité à l'aide de Robolectric.buildActivity(FragmentActivity::class.java)
  3. Initialisez l'activité en utilisant setup sur le contrôleur, cela permet de la détruire.
  4. Obtenez l'activité avec la méthode get du contrôleur.
  5. Obtenez votre modèle de vue en suivant les étapes décrites ci-dessus.
  6. Détruisez l'activité en utilisant destroy sur le contrôleur.
  7. Vérifiez le comportement de onCleared .

Exemple de classe complet ...

... basé sur l'exemple de la question:

@RunWith(RobolectricTestRunner::class)
class ViewModelOnClearedTest {
    @Test
    fun `MyViewModel#onCleared calls Object#function`() = mockkObject(Object) {
        val controller = Robolectric.buildActivity(FragmentActivity::class.java).setup()

        ViewModelProviders.of(controller.get()).get(MyViewModel::class.java)

        controller.destroy()

        verify { Object.function() }
    }
}

class MyViewModel : ViewModel() {
    override fun onCleared() = Object.function()
}

object Object {
    fun function() {}
}

0 commentaires

2
votes

Je viens de créer cette extension pour ViewModel:

/**
 * Will create new [ViewModelStore], add view model into it using [ViewModelProvider]
 * and then call [ViewModelStore.clear], that will cause [ViewModel.onCleared] to be called
 */
fun ViewModel.callOnCleared() {
    val viewModelStore = ViewModelStore()
    val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T = this@callOnCleared as T
    })
    viewModelProvider.get(this@callOnCleared::class.java)

    //Run 2
    viewModelStore.clear()//To call clear() in ViewModel
}


1 commentaires

Cela dépend de ViewModelStore # clear pour appeler ViewModel # onCleared , et donc cet appel est quelque peu caché parmi ces lignes de code. Cependant, cette solution est plutôt intéressante de par sa brièveté, pas besoin de réflexion ni de tests Robolectric / Android, et le fait que sur Android, c'est normalement une instance de ViewModelStore qui effectue l'appel. Merci pour votre réponse!



6
votes

Dans kotlin, j'ai trouvé que je pouvais remplacer la visibilité protégée en utilisant public , puis je peux l'appeler à partir d'un test.

class MyViewModel: ViewModel() {
    public override fun onCleared() {
        ///...
    }
}


1 commentaires

Un peu de @RestrictTo (RestrictTo.Scope.TESTS) en plus de onCleared () et tout va bien