El testing es una de las partes imprescindibles en el desarrollo de aplicaciones Android pero debido a su complejidad es una asignatura que se suele quedar en el olvido. Hoy empezamos nueva sección, dónde enseñare a implementar tests y sobre todo a entender lo que vamos haciendo.

¿Qué son los tests?

Los tests nos permiten verificar nuestro código para asegurarnos del correcto funcionamiento de este. Con ello podemos controlar las partes complejas de nuestra app y estar seguros de que cuando haya que hacer una nueva mejora no se estropee nada de lo anterior, proporcionándonos un código sólido y profesional. Gracias a los tests obtendremos:

  • Alertas sobre problemas o fallos.
  • Detectaremos rápidamente problemas mientras trabajamos en futuros desarrollos.
  • Simplifica los refactor (mejoras de código) ya que podremos optimizar funciones y asegurarnos de que seguirán funcionando.
  • Velocidad en el desarrollo y minimizar la deuda técnica.

Tipos de tests en Android

Tenemos distintos tipos de tests. Esta parte es algo más ambigua ya que dependerá bastante de la opinión de cada uno y el proyecto, pero creo que la mejor forma de empezar es dividiéndolos en tres distintos.

tipos de tests en android
Tipos de tests en Android.

En la imagen anterior apreciamos una pirámide muy similar a la alimenticia, ella nos muestra que lo más correcto (como norma general, siempre hay excepciones) es que la base o la mayor cantidad de tests sean unitarios, luego de integración y finalmente UI Tests.

  • Test unitarios: Son los principales tests que nos permiten comprobar el funcionamiento aislado de cada una de las funcionalidades.
  • Test de integración: Una vez funcionando los tests unitarios es el momento de comprobar que el flujo completo (o la integración) de todo lo que tenemos funciona.
  • UI Test: O test de interfaz gráfica nos aportan todo lo necesario para comprobar el funcionamiento de las vistas (por ejemplo cuando pulsas un botón comprobamos que se abre la pantalla correcta).

Antes de continuar, no quiero decir que los UI Tests por ejemplo sean peor que los Unit Tests, es justo lo contrario, la parte superior aumenta la fiabilidad pero también aumenta los tiempos de ejecución, desarrollo y esfuerzo para mantenerlos siempre al día. Como norma general habría que hacer un 70% de Unit tests, un 20% de Integration tests y un 10% de UI Tests. Pero te vuelvo a recordar que no es una ciencia cierta y depende de tu app.

Preparando nuestra aplicación Android para el testing

Antes de ponernos vamos a la obra vamos a asimilar algunos temas. Lo primero será el uso de librerías y es que para poder hacer testing es imprescindible el uso de estas.

En este caso como haremos Unit test necesitaremos las siguientes librerías para empezar.

    testImplementation 'junit:junit:4.+'
    testImplementation "io.mockk:mockk:1.12.2"

La primera librería viene por defecto en la creación del proyecto (como muchas otras). JUnit es un conjunto de herramientas IMPRESCINDIBLE para hacer tests unitarios. En esta ocasión usaremos la versión 4, porque aunque haya una versión 5, esta es la recomendada.

La segunda librería será Mockk, que será la encargada de preparar los mocks de nuestros tests.

¿Qué es un mock?

Un Mock es básicamente un objeto falso de una clase. Con él podemos trucar o alterar el resultado de funciones para poder testear lo que nos interese. Imaginemos que tenemos una clase la cual llama a otra para devolverle un true o un false. Si nosotros mockeamos esa clase podemos decirle antes de cada test que queremos que nos devuelva, si un true o un false.

Testing y directorios

En nuestro fichero build.gradle(app) podemos ver que tenemos dos tipos de librerías para los tests. Algunas empiezan con TestImplementation y otras con AndroidTestImplementation.

Esto se debe a que nuestros proyectos de Android tienen dos directorios distintos para los tests, AndroidTest y Test. Lo podemos ver de una forma sencilla si desplegamos nuestro proyecto y ponemos vista de Android.

directorios testing android
Directorios de testing para nuestra app.

Es por ello que las librerías que empiezan con TestImplementation solo servirán en el directorio Test y las que empiezan con AndroidTestImplementation solo valdrán en AndroidTest.

Esto está hecho porque el directorio que menciona a Android será el encargado de contener todos los tests que necesiten Android y por lo tanto un emulador u otras cosas que veremos más adelante (como por ejemplo los UI Tests). Por otro lado, el directorio sin Android no puede contener llamadas al framework de Android por lo que la ejecución de sus tests serán más rápidos tal y como vimos en la pirámide anterior.

Test unitarios en los casos de uso

Es el momento de empezar a testear y para ello lo haremos con los casos de uso de la capa del dominio. El proyecto que vamos a testear será una app MVVM que hemos ido haciendo desde cero, tienes el repositorio con los links a todos los tutoriales aquí. Y te dejo por aquí la lista de capítulos por escrito.

Crear la clase test

Como norma general, la clase que testea a otra se llamará igual pero con el sufijo Test. En este caso empezaremos con la clase GetQuotesUseCase, el nombre del fichero que lo testea debería ser GetQuotesUseCaseTest.

La clase la podemos crear manualmente pero tenemos una forma mucho más cómoda de hacerlo ya que lo correcto es que esté en un directorio similar al que está el caso de uso. Para ello, una vez tengamos el caso de uso abierto, iremos al menú superior Navigate>Test y al darle nos saldrá un mensaje para crear una clase test. Si por el contrario ya estuviera creada nos llevaría directamente a esa.

dialogo de creacion de clase test
Ejemplo de creación de clase de test.


Haremos clic en Create New Test y nos saldrá un wizard para seleccionar toda la configuración de dicha clase.

wizard de creación de tests en android studio
Diálogo de creación de clase Test.

Si os fijáis ya de por sí creará el directorio similar a donde tenemos nuestro caso de uso pero en la parte de test, mantendremos JUnit4 y el nombre aunque se puede editar recomiendo dejarlo como nos lo genera. De resto no seleccionaremos nada más ya que nosotros lo haremos todo. Pulsamos en OK.

selección de directorio de test en android studio
Selección de directorio donde queremos crear nuestra clase test.

Una vez nuestra clase esté creada tendremos que configurarlo. Pensad que lo que vamos a testear es el propio caso de uso y por lo cual tendremos que mockear el resto.

Si ya has visto el proyecto verás que la clase GetQuotesUseCase recibe como parámetro inyectado un repositorio. La idea es mockear dicho repositorio para poder «trucar» o manipular las respuestas.

Before y After

Un test es muy similar a una función, la única diferencia es la anotación de la parte superior. Pensemos que cuando lanzamos una batería de tests, estos se ejecutan seguidos pero es probable que necesiten una configuración inicial o final.

Es por ello que tenemos las etiquetas @Before y @After y gracias a ellas podemos hacer configuraciones genéricas para la clase. Por ejemplo crear una instancia de algo o reiniciar valores.

En este caso en específico necesitamos configurar nuestra librería MockK y para ello tenemos que inicializarlo antes de lanzar los tests, por lo que lo añadiremos en la función que contenga la etiqueta @Before.

@Before
    fun onBefore() {
        MockKAnnotations.init(this)
    }

Mockeando el repositorio

Para definir un mock solo debemos añadir la etiqueta @MockK o @RelaxedMockK. La diferencia entre estas es que si decimos que es relaxed y no controlamos la respuesta de una de sus funciones el propio sistema nos dará una por defecto, así que si por el contrario seleccionamos solamente @MockK tendremos que configurar cada una de las respuestas que pudiera darnos esa clase mockeada.

@RelaxedMockK
private lateinit var quoteRepository: QuoteRepository

Simplemente con esa etiqueta y la línea que pusimos en el @Before ya estaría configurado. Ahora ya podemos usar ese repositorio como si fuera real y podremos crear nuestra instancia de GetQuotesUseCase que iniciaremos dentro del onBefore. Una vez hecho eso nuestra clase quedaría así.

class GetQuotesUseCaseTest {

    @RelaxedMockK
    private lateinit var quoteRepository: QuoteRepository

    lateinit var getQuotesUseCase: GetQuotesUseCase

    @Before
    fun onBefore() {
        MockKAnnotations.init(this)
        getQuotesUseCase = GetQuotesUseCase(quoteRepository)
    }
    
}

Creando nuestro primer test

Ha llegado el momento, tenemos todo preparado y muchas ganas de testear. Lo primero es pensar que vamos a probar.

Este caso de uso es bastante sencillo, simplemente llamará al repositorio y dependiendo de la respuesta hará una cosa u otra, pues ya tenemos dos posibles tests.

  • Para el primer test comprobaremos que si la respuesta del repositorio es vacía, entonces deberá llamar a la base de datos.
  • El segundo test tendrá que probar que se llama a las funciones y retorna el valor devuelto.

Los nombres son muy importantes en los tests, tienen que ser extremadamente descriptivos. Es por ello que suelen ser muy largos y tenemos dos formas de nombrarlos. Si cogemos de ejemplo el primer test que tenemos que hacer podríamos llamarlo

fun whenTheApiDoesntReturnAnythingThenGetValuesFromDatabase() = runBlocking{}

O podemos aprovecharnos del acento grave o comúnmente conocido como tildes invertidas (`) que nos permiten poner espacios en el nombre de la función.

fun `when the api doesnt return anything then get values from database`() = runBlocking{}

Mucho más legible ¿Verdad?

Como nuestros tests utilizarán corrutinas deberemos usar un runBlocking{} que nos permite lanzar una corrutina y así hacer funcionar el test.

Given When Then

Given-when-then es un estilo semi estructurado para representar los tests, esto nos ayudará a entender los test y sobre todo a dar ese primer paso para escribirlos. Si lo extrapolamos a nuestro primer test tendríamos algo así.

  • Given: A nuestro mock (el repositorio) le tenemos que dar la respuesta que queremos que devuelva, en este caso una lista vacía.
  • When: Llamamos a nuestro caso de uso.
  • Then: Tenemos que verificar que la función correcta del repositorio ha sido llamada.
@Test
    fun `when the api doesnt return anything then get values from database`() = runBlocking {
        //Given
        coEvery { quoteRepository.getAllQuotesFromApi() } returns emptyList()

        //When
        getQuotesUseCase()

        //Then
        coVerify(exactly = 1) { quoteRepository.getAllQuotesFromDatabase() }
    }

Analicemos el código anterior. Para mockear una respuesta solo debemos llamar a coEvery (si la función es una corrutina) o every, dentro añadiremos la función que deberá ser llamada y luego ponemos returns (con S final) y ponemos la respuesta que queremos que devuelva.

Acto seguido solo llamamos a nuestro caso de uso, ya que este es real (no mockeado) y necesitamos que se ejecute para comprobar que todo va como esperamos.

Finalmente usamos coVerify o verify (igual que antes, dependiendo si es una corrutina) y podemos preguntar si la función de dentro ha sido llamada. El exactly = 1 que hemos puesto en el parámetro del coVerify es para asegurar que se ha llamado exactamente una única vez, pero no es obligatorio.

Para ejecutarlo solo tenemos que pulsar a la izquierda del nombre.

Ejecutando test en android studio
Ejecutando test en Android Studio.

Si todo ha salido bien se abrirá la parte inferior de run y nos dará el resultado del test, si ha fallado ahí estará el error.

run test Android Studio
Run testing.

Pasemos al segundo test.

  • Given: Nuestro repositorio debe retornar una lista de Quotes
  • When: Cuando llamamos al caso de uso
  • Then: Verificamos los métodos que se llaman y comprobamos que la respuesta del caso de uso es la misma que le dimos al repositorio.
 @Test
    fun `when the api return something then get values from database`() = runBlocking {
        //Given
        val myList = listOf(Quote("Déjame un comentario", "AristiDevs"))
        coEvery { quoteRepository.getAllQuotesFromApi() } returns myList

        //When
        val response = getQuotesUseCase()

        //Then
        coVerify(exactly = 1) { quoteRepository.clearQuotes() }
        coVerify(exactly = 1) { quoteRepository.insertQuotes(any()) }
        coVerify(exactly = 0) { quoteRepository.getAllQuotesFromDatabase() }
        assert(response == myList)
    }

Este test es muy similar al anterior, en el given he creado una variable en vez de poner directamente el contenido en el returns, ya que lo volveremos a usar en la parte final del test. El when es completamente igual y en el then verificamos que se nombren unos métodos y otro no (con el exactly = 0) y con la función assert podemos verificar si la respuesta del caso de uso es la misma que la del repositorio (que debería serlo).

Con esto ya tenemos nuestro primer caso de uso terminado. Aquí te dejo el link a la clase completa.

Testeando nuestro segundo caso de uso

En esta ocasión vamos a ir más rápido que ya tenemos los conceptos explicados, te recuerdo que tienes el vídeo del artículo donde explico todo mas detalladamente.

Este caso de uso recupera de la base de datos las citas y una vez hecho eso devuelve una de manera aleatoria. Si no hay citas guardadas devuelve null. Antes de continuar piensa que tests podrías hacer.

Empezaremos configurando la clase.

class GetRandomQuoteUseCaseTest {
    @RelaxedMockK
    private lateinit var quoteRepository: QuoteRepository

    lateinit var getRandomQuoteUseCase: GetRandomQuoteUseCase

    @Before
    fun onBefore() {
        MockKAnnotations.init(this)
        getRandomQuoteUseCase = GetRandomQuoteUseCase(quoteRepository)
    }

}

Casi igual al caso de uso anterior. Ahora definiremos el primer test, el cual devolverá un listado vacío de la base de datos por lo cual a[[tiene que retornar un null.

   @Test
    fun `when database is empty then return null`() = runBlocking {
        coEvery { quoteRepository.getAllQuotesFromDatabase() } returns emptyList()

        val response = getRandomQuoteUseCase()

        assert(response == null)
    }

Este test no tiene nada nuevo por lo que pasamos al siguiente.

  @Test
    fun `when database is not empty then return quote`() = runBlocking {
        val quoteList = listOf(Quote("Holi", "AristiDevs"))

        coEvery { quoteRepository.getAllQuotesFromDatabase() } returns quoteList

        val response = getRandomQuoteUseCase()

        assert(response == quoteList.first())
    }

Esta vez hay que ser algo más pícaro y es que al retornar un item del listado aleatorio es mas complicado de probar, por lo que el listado solamente contendrá un objeto y si todo funciona debería devolver ese.

Puedes ver la clase completa en GitHub.

Testing unitario avanzado

Es el momento de testear nuestro ViewModel, el cual va a ser algo más complejo pero es el momento perfecto para aprender nuevos conceptos. Además con la llegada de las corrutinas 1.6 acaba de cambiar.

Añadiendo dependencias

Para esta parte tendremos que añadir nuevas dependencias. No lo he puesto en el principio porque quiero que entendáis el uso de cada librería.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' //Ya estaba, la hemos actualizado.
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"
testImplementation "androidx.arch.core:core-testing:2.1.0"

¿Por qué necesitamos nuevas librerías? Pues para trabajar con LiveData y poder testearlos es obligatorio usar arch.core y necesitaremos las coroutines-test para poder crear dispatcher que explicaremos más adelante.

Testing en ViewModel

Nuestro ViewModel era muy sencillo, contenía dos métodos, el primero llamaba a getQuotesUseCase() que recuperaba todos los quotes y el segundo llamaba al getRandomQuoteUseCase() que devolvía una cita y la insertaba en el LiveData para que apareciera en la vista. Crearemos tres test.

  • El primero emulará el funcionamiento de cuando abrimos la app, que recupera el listado de quotes y muestra el primero, replicaremos eso.
  • El segundo test comprobará nuestro «happy path» es decir, el flujo normal y óptimo de la app, que cuando se llame al randomQuote() se le asigne dicho valor a nuestro LiveData.
  • Terminaremos comprobando lo contrario, si tenemos un valor ya en nuestro LiveData y la función randomQuote() devuelve null tendremos que mantener el último valor funcional.

Preparando ViewModelTest

Creamos nuestra clase test como hemos visto anteriormente.

class QuoteViewModelUnitTest {

    @RelaxedMockK
    private lateinit var getQuotesUseCase: GetQuotesUseCase

    @RelaxedMockK
    private lateinit var getRandomQuoteUseCase: GetRandomQuoteUseCase

    private lateinit var quoteViewModel: QuoteViewModel

    @Before
    fun onBefore() {
        MockKAnnotations.init(this)
        quoteViewModel = QuoteViewModel(getQuotesUseCase, getRandomQuoteUseCase)
    }

}

Hasta aquí sería una clase test normal, hemos mockeado los dos casos de uso ya que los necesitamos para instanciar el ViewModel. El siguiente paso será crear una regla.

Reglas y dispatcher

Llega una nueva parte de la cual no hemos hablado. Al añadir estas últimas librerías comentaba que arch.core era la que nos permitiría testear los LiveData y para ello hay que crear una regla.

Las reglas nos permiten ser más flexibles y reducir el boilerplate de nuestras clase, es básicamente es el mismo comportamiento que el @Before pero nos permite reutilizar el código, por ejemplo podríamos crear una regla para inicializar los mocks.

@get:Rule
var rule: InstantTaskExecutorRule = InstantTaskExecutorRule()

Esta clase ya viene dada por la librería anteriormente mencionada así que solo tenemos que añadirlo en la parte superior de nuestra clase.

Recuerda que en nuestro ViewModel usamos la función viewModelScope lo que confirma que estamos trabajando con corrutinas y es por ello que tenemos que modificar el dispatcher.

Para explicarlo de una manera sencilla, los dispatcher son los que gestionan los hilos que usarán nuestras corrutinas y al estar haciendo testing tendremos que «trucarlo». Para ello definiremos nuestro propio dispatcher.

    @Before
    fun onBefore() {
        MockKAnnotations.init(this)
        quoteViewModel = QuoteViewModel(getQuotesUseCase, getRandomQuoteUseCase)
        Dispatchers.setMain(Dispatchers.Unconfined)
    }

    @After
    fun onAfter() {
        Dispatchers.resetMain()
    }

Fíjate en las lineas destacadas, en nuestra función onBefore hemos añadido el dispatcher y la función onAfter la cual hablamos al principio pero no utilizamos, nos servirá para reiniciarlo al terminar los tests.

Creando los tests

Una vez todo preparado es el momento de crear nuestro primer test.

    @Test
    fun `when viewmodel is created at the first time, get all quotes and set the first value`() = runTest{
        //Given
        val quote = listOf(Quote("Holi", "Aris"), Quote("Dame un like", "Otro Aris "))
        coEvery { getQuotesUseCase() } returns quote

        //When
        quoteViewModel.onCreate()

        //Then
        assert(quoteViewModel.quoteModel.value == quote.first())
    }

Como comentaba antes este tests es muy sencillo, solo devolvemos un listado de citas y nos aseguramos que a nuestro ViewModel se le asigna el primero de dicha lista. Fijaros que en vez de runBlocking usamos runTest, lo necesitaremos en cada uno de nuestros test del ViewModel.

    @Test
    fun `when randomQuoteUseCase return a quote set on the livedata`() = runTest {
        //Given
        val quote = Quote("Holi", "Aris")
        coEvery { getRandomQuoteUseCase() } returns quote

        //When
        quoteViewModel.randomQuote()

        //Then
        assert(quoteViewModel.quoteModel.value == quote)
    }

Creo que este test con lo sencillo que es y el título que tiene se explica solo.

  @Test
    fun `if randomQuoteUseCase return null keep the last value`() = runTest{
        //Given
        val quote = Quote("Aris", "Aris")
        quoteViewModel.quoteModel.value = quote
        coEvery { getRandomQuoteUseCase() } returns null
        
        //When
        quoteViewModel.randomQuote()

        //Then
        assert(quoteViewModel.quoteModel.value == quote)
    }

Este último tests es muy similar pero empezamos asignandole un valor por defecto a nuestro LiveData (quoteModel) para comprobar que si nuestro caso de uso retorna un null en vez de asignarlo mantiene el último valor.

Hasta aquí el capítulo de hoy, recuerda que el testing es imprescindible para convertirte en un desarrollador Android profesional así que te recomiendo que le des mucha caña.

Recuerda que puedes descargarte el proyecto entero y separado por ramas (para poder seguir todos los capítulos del curso) en mi Github. Además recuerda que este capítulo está en vídeo para que lo entiendas de una forma más sencilla.

Siguiente curso: Jetpack Compose


Te recuerdo que puedes seguirme en mis redes sociales en Aristi.Dev. Y si tienes dudas con este o cualquier otro artículo del blog únete al Discord de la comunidad y te ayudaremos.