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.
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)
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"!
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)