DEV Community

Cover image for Como testar um LiveData?
vitorbranco for Comunidade Dev Space

Posted on

Como testar um LiveData?

A criação de aplicativos modernos e eficientes é fundamental para proporcionar uma experiência excepcional ao usuário. Nesse sentido, a arquitetura MVVM (Model-View-ViewModel) tem se destacado como uma abordagem poderosa para o desenvolvimento de aplicativos Android, permitindo uma separação clara das responsabilidades e facilitando a manutenção e escalabilidade do código. Dentro dessa arquitetura, um dos componentes fundamentais é o LiveData.

O que é LiveData?

O LiveData é um classe observável que permite a comunicação eficiente entre as camadas do aplicativo, tornando possível a atualização automática da UI com base em mudanças nos dados em tempo real.
Algumas vantagens do uso do LiveData, além das atualizações automáticas da UI, são:

  • Gerenciamento de ciclo de vida: o LiveData integra-se com o ciclo de vida dos componentes do Android, que evita problemas comuns, como vazamentos de memória.
  • Dados sempre atualizados: quando um ciclo de vida se torna inativo, ele recebe os dados mais recentes quando se tornar ativo novamente, como no caso de uma rotação de tela.
  • Tratamento de eventos assíncronos: o LiveData oferece um mecanismo para observar e reagir a eventos assíncronos, como uma chamada de rede, por exemplo.

Agora que compreendemos sua importância, como garantir que nosso LiveData está transmitindo as informações corretamente? Vamos ver a seguir como realizar testes unitários efetivos para garantir a qualidade e a estabilidade do código contendo LiveData.

Como testar um LiveData?

Para testar um LiveData é muito importante lembrar que o LiveData é uma classe observável, ou seja, depende de um Observer para atualizar os dados. A maioria dos erros ao escrever um teste é esquecer de observá-lo no teste. Assim, são necessários alguns passos para escrever o teste de maneira assertiva, que veremos a seguir.

Criando um ViewModel

Neste artigo, iremos usar como exemplo um ViewModel (AlunoViewModel) que define um MutableLiveData de String, e que tem uma função chamada setNomeDoAluno() que é responsável por alterar o nome de um aluno hipotético, que está guardado no LiveData:

class AlunoViewModel: ViewModel() {
    private val _alunoLiveData = MutableLiveData<String>()
    val alunoLiveData: LiveData<String> = _alunoLiveData

    fun setNomeDoAluno(nome: String) {
        _alunoLiveData.value = nome
    }
}
Enter fullscreen mode Exit fullscreen mode

Criando o teste

Dentro do diretório de testes, criamos uma classe AlunoViewModelTest e dentro dela definimos uma variável underTest que é uma instância de AlunoViewModel para podermos utilizar durante os testes.

Escrevemos então a função isLiveDataWorking() com a notação @Test (da biblioteca do JUnit), e nela utilizamos a função setNomeDoAluno() para alterar o nome do aluno e passá-lo para o LiveData. Em seguida usamos o assertEquals para comparar o valor emitido pelo LiveData com o valor esperado:

class AlunoViewModelTest {
    private val underTest: AlunoViewModel by lazy {
        AlunoViewModel()
    }

    @Test
    fun `isLiveDataWorking`() {
        underTest.setNomeDoAluno("Vitor")
        assertEquals(underTest.alunoLiveData.value, "Vitor")
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao rodar o teste acima, de cara temos o nosso primeiro erro! Não se assuste, esse erro é extremamente comum.

Imagem com o erro 'Method getMainLooper in android.os.Looper not mocked.'

Esse erro ocorre porque, por padrão, o LiveData executa as tarefas em segundo plano usando um executor em uma thread separada, o que pode causar problemas nos testes unitários, pois os resultados podem não estar prontamente disponíveis para verificação.

Para resolver esse problema, nós usamos a regra InstantTaskExecutorRule, que veremos a seguir.

InstantTaskExecutorRule

Essa é uma regra de testes usada para substituir o comportamento padrão do LiveData relacionado à execução de tarefas em segundo plano. O InstantTaskExecutorRule garante que as tarefas sejam executadas imediatamente no encadeamento de teste, permitindo que os testes obtenham resultados corretos e façam as verificações necessárias.

Para utilizá-lo, basta adicionar a regra da seguinte maneira:

class AlunoViewModelTest {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private val underTest: AlunoViewModel by lazy {
        AlunoViewModel()
    }

    @Test
    fun `isLiveDataWorking`() {
        underTest.setNomeDoAluno("Vitor")
        assertEquals(underTest.alunoLiveData.value, "Vitor")
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora sim, nosso teste estará funcionando.
Mas e caso eu precise fazer alguma transformação no meu LiveData? Suponhamos que eu precise de um LiveData que me dê o número de caracteres do nome do aluno:

class AlunoViewModel: ViewModel() {
    private val _alunoLiveData = MutableLiveData<String>()

    // Mapeia os dados do alunoLiveData para retornar o número de caracteres
    val numeroDeCaracteresLiveData = _alunoLiveData.map { it.length }

    fun setNomeDoAluno(nome: String) {
        _alunoLiveData.value = nome
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao realizar o mesmo procedimento do teste que fizemos com o alunoLiveData, alterando o nosso assertEquals, temos outro erro:

assertEquals(underTest.numeroDeCaracteresLiveData.value, 5)

Imagem do teste falho: nosso resultado esperado não bateu com o resultado obtido.

O teste falhou, uma vez que o LiveData nos retornou null como valor, ao invés de 5 que era o esperado para o aluno "Vitor". Isso acontece porque, apenas ler o valor de numeroDeCaracteresLivedata.value não inicia a corrente de transformações pois ele não está sendo observado!

No primeiro teste que fizemos, o alunoLiveData teve seu valor lido pois ele é um MutableLiveData e nenhuma transformação foi feita nele.

Vamos resolver esse problema colocando um Test Helper.

Criando um Test Helper

Poderíamos utilizar a função observeForever() para observar os dados do LiveData durante o teste, mas isso não é o mais adequado.

É mais recomendado recorrer a um Test Helper, que nada mais é do que uma função a ser acrescentada na classe LiveData que permite buscar o valor do nosso LiveData por um tempo determinado e, depois de encontrado o valor, remover o Observer (coisa que o observeForever() não faz).

Além disso, o Test Helper exibe um erro caso nenhum valor tenha sido obtido após determinado tempo, o que impede que o teste fique em execução eternamente.

Crie um novo arquivo (do tipo File mesmo) e adicione o código fornecido pela Google para ajudar nos testes de LiveData:

fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(value: T) {
            data = value
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }

    this.observeForever(observer)

    if (!latch.await(time, timeUnit)) {
        throw TimeoutException("LiveData value was never set.")
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}
Enter fullscreen mode Exit fullscreen mode

Não se esqueça de adicionar corretamente todas as dependências necessárias nesse arquivo:

import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
Enter fullscreen mode Exit fullscreen mode

A função getOrAwaitValue() não existe na classe LiveData, mas com esse código nós estamos adicionando ela nessa classe, e então podemos usá-la no nosso teste:

class AlunoViewModelTest {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private val underTest: AlunoViewModel by lazy {
        AlunoViewModel()
    }

    @Test
    fun `isLiveDataWorking`() {
        underTest.setNomeDoAluno("Vitor")
        assertEquals(underTest.numeroDeCaracteresLiveData.getOrAwaitValue(), 5)
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao rodar o teste dessa maneira, temos então o nosso tão almejado "Tests passed"!

Resultado exibindo "Tests passed"

Sobre mim

Engenheiro e agora Desenvolvedor Android em transição de carreira, sempre em busca de novos conhecimentos e experiências. Confira meu perfil no LinkedIn e no GitHub, e sinta-se a vontade para qualquer contato!

👔 https://www.linkedin.com/in/vitor-xatara-branco/
💻 https://github.com/vitorbranco

Top comments (0)