Obtendo uma exceção de ponteiro nulo ao zombar e espiar em uma classe de teste

8
Android Studio 3.5.3
Kotlin 1.3

Estou tentando testar algum código simples, mas continuo recebendo a seguinte exceção:

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

Estou usando o espião e zombando do retorno para que ele retorne um valor nulo. Como eu quero testar o caminho do erro.

Não tenho certeza se estou fazendo algo errado com o meu stubbing ou não. Mas parece que não consigo resolver essa exceção.

Usando uma classe wrapper para agrupar a implementação gson e espionando isso no teste

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);
    }
}

Implementação da minha classe que está em teste

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
}

A classe de teste real, mantendo tudo simples

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
    }
}

Muito obrigado por todas as sugestões,

ant2009
fonte

Respostas:

3

Depende do que você deseja alcançar. Deseja permitir MoviePresenterImp.serializeStringToMapretornar null? No momento, não é possível e é isso que você está testando no seu teste de unidade:

  • o que acontecerá quando gsonWrapper.fromJsonretornar null?

  • serializeStringToMap lançará uma exceção porque seu tipo de retorno é declarado como não nulo (o Kotlin adiciona uma verificação nula sob o capô).

De fato, spyGsonWrapper.fromJsonsó retorna nullse gson.fromJsonretornar null. De acordo com os documentos java do Gson, isso pode acontecer apenas se o jsonargumento for null(se jsonfor inválido, o método lança JsonSyntaxException). Então você deve:

  • verifique se o jsonparâmetro está nullno spyGsonWrapper.fromJsone jogue IllegalArgumentException, se estiver. Isso garantirá que o método nunca retorne null(entre. Você pode adicionar uma @NotNullanotação, consulte Anotações de nulidade ). Você pode continuar serializeStringToMapcomo está, mas precisa alterar o teste, porque não faz mais sentido.
  • se você preferir retornar em nullvez de lançar uma exceção, precisará alterar o MoviePresenterImp.serializeStringToMap, conforme sugerido por @ duongdt3

Aqui está um exemplo de teste:

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
        // ...
    }
}
Mafor
fonte
6

Encontrei problemas aqui:

Você deve usar o mapa anulável? em vez de um mapa não nulo em MoviePresenterImp (código Kotlin), porque na classe Unit Test, você espia gsonWrapper e força o método 'spyGsonWrapper.fromJson' retorna nulo.

Está tudo bem agora.

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
    }
duongdt3
fonte
Oi, sim, isso funcionou. No entanto, não quero retornar um mapa nulo e gostaria de mantê-lo como a non-null. Eu acho que o que estou tentando alcançar é retornar um mapa que tenha conteúdo nulo. Então, quando movieMap.getOrElse()retornará um nulo. Era isso que eu estava tentando zombar. Ter o movieMap contendo um nulo é algo que não sei ao certo como fazer ?.
Ant2009
1
Na verdade, sobre a análise de JSON com Gson, você deve suportar casos nulos, às vezes o texto JSON não corresponde à nossa expectativa. Você tem um caso de teste para null é uma boa ideia.
duongdt3
2

Com sua configuração que você está procurando, em emptyMap()vez denull

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

Isso cumprirá a assinatura, pois não é nulo

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

Além disso, ele inserirá o bloco else na movieMap.getOrElse()chamada.

tynn
fonte