web-dev-qa-db-fra.com

Obtention d'une exception de pointeur nul lors de moqueries et d'espionnage dans une classe de test

Android Studio 3.5.3
Kotlin 1.3

J'essaie de tester du code simple mais je reçois toujours l'exception suivante:

IllegalStateException: gsonWrapper.fromJson<Map…ring, String>>() {}.type) must not be null

J'utilise l'espion et je me moque du retour afin qu'il renvoie un null. Comme je veux tester le chemin d'erreur.

Je ne sais pas si je fais quelque chose de mal avec mon tronçonnage ou non. Mais ne semble pas résoudre cette exception.

Utiliser une classe wrapper pour encapsuler l'implémentation gson et l'espionner dans le test

public class GsonWrapper implements IGsonWrapper {

    private Gson gson;

    public GsonWrapper(Gson gson) {
        this.gson = gson;
    }

    @Override public <T> T fromJson(String json, Type typeOf) {
        return gson.fromJson(json, typeOf);
    }
}

Implémentation de ma classe en cours de test

class MoviePresenterImp(
        private val gsonWrapper: IGsonWrapper) : MoviePresenter {

    private companion object {
        const val movieKey = "movieKey"
    }

    override fun saveMovieState(movieJson: String) {
            val movieMap = serializeStringToMap(movieJson)

            when (movieMap.getOrElse(movieKey, {""})) {
                /* do something here */
            }
    }

    // Exception return from this method
    private fun serializeStringToMap(ccpaStatus: String): Map<String, String> =
            gsonWrapper.fromJson<Map<String, String>>(ccpaStatus, object : TypeToken<Map<String, String>>() {}.type) // Exception
}

La classe de test réelle, en gardant tout simple

class MoviePresenterImpTest {
    private lateinit var moviePresenterImp: MoviePresenterImp
    private val gsonWrapper: GsonWrapper = GsonWrapper(Gson())
    private val spyGsonWrapper = spy(gsonWrapper)

    @Before
    fun setUp() {
        moviePresenterImp = MoviePresenterImp(spyGsonWrapper)
    }

    @Test
    fun `should not save any movie when there is an error`() {
        // Arrange
        val mapType: Type = object : TypeToken<Map<String, String>>() {}.type
        whenever(spyGsonWrapper.fromJson<Map<String, String>>("{\"movie\":\"movieId\"}", mapType)).thenReturn(null)

        // Act
        moviePresenterImp.saveMovieState("{\"movie\":\"movieId\"}")

        // Assert here
    }
}

Merci beaucoup pour toutes suggestions,

8
ant2009

Cela dépend de ce que vous voulez réaliser. Voulez-vous autoriser MoviePresenterImp.serializeStringToMap pour retourner null? Pour le moment ce n'est pas possible et c'est ce que vous testez dans votre test unitaire:

  • que se passera-t-il quand gsonWrapper.fromJson renvoie null?

  • serializeStringToMap lèvera une exception car son type de retour est déclaré non nullable (Kotlin ajoute un null-check sous le capot).

En réalité,spyGsonWrapper.fromJson renvoie uniquement null si gson.fromJson renvoie null. Selon les documents Java Java de Gson, cela ne peut se produire que si l'argument json est null (si json n'est pas valide, la méthode lance JsonSyntaxException). Vous devez donc:

  • vérifier si le paramètre json est null dans le spyGsonWrapper.fromJson et lancez IllegalArgumentException si c'est le cas. Cela garantira que la méthode ne renvoie jamais null (btw. Vous pouvez ajouter un @NotNull annotation, voir annotations-nullité ). Vous pouvez conserver serializeStringToMap tel quel, mais vous devez modifier le test, car cela n'a plus de sens.
  • si vous préférez retourner null au lieu de lever une exception, vous devez modifier MoviePresenterImp.serializeStringToMap comme suggéré par @ duongdt3

Voici un exemple de test:

class MoviePresenterImpTest {

    private lateinit var moviePresenter: MoviePresenterImp
    private lateinit var spyGsonWrapper: GsonWrapper

    @Rule @JvmField
    var thrown = ExpectedException.none();

    @Before
    fun setUp() {
        spyGsonWrapper = Mockito.mock(GsonWrapper::class.Java)
        moviePresenter = MoviePresenterImp(spyGsonWrapper)
    }

    @Test
    fun `should not save any movie when GsonWrapper throws an error`() {
        // Given
        Mockito.`when`(spyGsonWrapper.fromJson<Map<String, String>>(anyString(), any(Type::class.Java)))
            .thenThrow(JsonSyntaxException("test"))
        // Expect
        thrown.expect(JsonSyntaxException::class.Java)
        // When
        moviePresenter.saveMovieState("{\"movie\":\"movieId\"}")
    }

   // Or without mocking at all

    @Test
    fun `should not save any movie when Gson throws error`() {
        // Given
        moviePresenter = MoviePresenterImp(GsonWrapper(Gson()))
        // Expect
        thrown.expect(JsonSyntaxException::class.Java)
        // When
        moviePresenter.saveMovieState("Some invalid json")
    }

    // If you want to perform additional checks after an exception was thrown
    // then you need a try-catch block

    @Test
    fun `should not save any movie when Gson throws error and `() {
        // Given
        moviePresenter = MoviePresenterImp(GsonWrapper(Gson()))
        // When
        try {
            moviePresenter.saveMovieState("Some invalid json")
            Assert.fail("Expected JsonSyntaxException")
        } catch(ex : JsonSyntaxException) {}
        // Additional checks
        // ...
    }
}
3
Mafor

J'ai trouvé des problèmes ici:

Vous devez utiliser la carte nullable? au lieu d'une carte non nulle dans MoviePresenterImp (code Kotlin), car dans la classe Unit Test, vous espionnez gsonWrapper et forcez la méthode 'spyGsonWrapper.fromJson' à retourner null.

Ça va maintenant.

fun saveMovieState(movieJson: String) {
        val movieMap = serializeStringToMap(movieJson)

        when (movieMap?.getOrElse(movieKey, { "" })) {
            /* do something here */
        }
    }

    // Exception return from this method
    private fun serializeStringToMap(ccpaStatus: String): Map<String, String>? {
        val type: Type =
            object : TypeToken<Map<String, String>>() {}.type
        return gsonWrapper.fromJson(ccpaStatus, type) // Exception
    }
6
duongdt3

Avec votre configuration, vous recherchez emptyMap() au lieu de null

whenever(spyGsonWrapper.fromJson<Map<String, String>>("{\"movie\":\"movieId\"}", mapType))
    .thenReturn(emptyMap())

Ce sera conforme à la signature, car il est non nul

fun serializeStringToMap(ccpaStatus: String): Map<String, String>

Il entrera également le bloc else dans l'appel movieMap.getOrElse().

2
tynn