Como testar um LiveData?
Published: 2023-07-13 17:09:36

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


**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")
    }
}
```

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.'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnh96surykjf5yn8sqis.png)

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")
    }
}
```
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
    }
}
```

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3lluj6lfh5zslh43h38.png)

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

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

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

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


![Resultado exibindo "Tests passed"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mue6vwi1yc5rnt47lnd7.png)


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