Testing en Android
Tags: android, testing, kotlin
Published: 2023-05-22 14:27:21

- [Tipos de Tests](#tipos-de-tests)
- [Directorios del proyecto](#directorios-del-proyecto)
    - [Directorio `tests`](#directorio-tests)
    - [Directorio `androidTests`](#directorio-androidtests)
- [Ejemplos](#ejemplos)
    - [Test unitarios](#test-unitarios)
    - [Test de integración](#test-de-integración)
    - [Test de IU](#test-de-iu)

----

# Tipos de Tests

Los tres tipos de tests que se realizan en Android *(en general en cualquier lenguaje/framework son iguales)* son los siguientes:

- **Unit Tests**: tests por unidad, es decir testear las clases o funciones de forma individual y verificar que cada una haga lo que se supone que debe hacer.
- **Integration Tests**: tests que validan la integración entre diferentes módulos
- **UI Tests**: tests de la interfaz del usuario, o end to end

![Pirámide testing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/de7026u8o87odnxfmlc8.png)

Google recomienda dividir los tipos de tests de la siguiente forma:

|Tipo Test | Porcentaje |
| ----| ------|
|Unit tests | 70% |
|Integration Test | 20% |
|UI Tests | 10% |


# Directorios del proyecto

Existen dos formas de ejecutar los tests en Android:

1. En la maquina local, mediante la JVM.
2. En un dispositivo real *(o emulado)*.

Dependiendo de donde se quieran ejecutar las pruebas *(en local o real)* existen dos directorios en el proyecto:

1. `com/example/project/tests/`: **local** 
2. `com/example/project/androidTests/`: **real**

## Directorio tests

En dicho directorio se guardaran todos los **test** relacionados con la **ejecución** en la **maquina local**.

Por lo general serán los **tests unitarios** los que se guarden en dicho directorio porque no acceden a recursos del dispositivo y tampoco tienen que interactuar con el dispositivo.

## Directorio androidTests

En este directorio se guardaran todos los **tests** que se vayan a **ejecutar** en el **dispositivo** real  o en un **emulador**.

Los tipos de tests que se guardarán en dicho directorios son **tests de integración** y **e2e** *(end-to-end)*.


# Ejemplos

> En los ejemplos siguientes, no muestro como añadir las dependencias necesarias para los tests y tampoco menciono que dependencias son necesarias. Consultar la guía oficial en las [referencias](#referencias) para ver la implementación completa.

## Test unitarios

Estos tipos de tests a mi parecer son los mas sencillos de entender, ya que pruebas cosas de forma individual, funciones o métodos de clases.

Imagina que vamos a querer comprobar dos métodos de la clase `MyFunctions`:

- El primer método se encarga de sumar dos números.
- El segundo devuelve la fecha de hoy en formato `dd-MM-yyyy`

*Son dos métodos muy básicos pero útiles para entender las pruebas unitarias*

#### Clase a testear
```kotlin
class MyFunctions {
    fun add(a: Int, b: Int): Int {
        return a + b
    }

   fun getCustomDate(): String {
        val date = Date()
        val format = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
        return format.format(date)
    }
}
```

Para testear dichas funciones vamos a crear el fichero `com.example.project/tests/MyFuncionsTests.kt` y dentro estará la lógica que comprobará que cada función hace lo que debe hacer.

> Una buena práctica para realizar tests es provocar que la aplicación falle de cualquier manera. Es decir utilizarla de la forma en la que no se había pensado.

> Por ejemplo, si un input está pensado para introducir números, vamos a introducir texto a ver que ocurre. Debemos pensar como un usuario y no como el desarrollador que ha implementado dicha funcionalidad.


Para realizar los tests utilizamos `JUnit` que nos provee de las anotaciones `@Before`, `@After` y `@Test` entre otras.

```kotlin
class MyFunctionsTest {
    private lateinit var myFunctions: MyFunctions

	// Se ejecuta antes de ejecutar cada test
    @Before
    fun setUp() {
        myFunctions = MyFunctions()
    }

	/*             Tests            */
	
    @Test
    fun `add function correct addition`() {
        val resultado = myFunctions.add(3, 2)
        assertEquals(5, resultado)
    }

    @Test
    fun `add function incorrect addition`() {
        val resultado = myFunctions.add(3, 2)
        assertNotEquals(6, resultado)
    }

    @Test
    fun `get custom date format dd-MM-yyyy`() {
        val fechaEsperada = SimpleDateFormat("dd-MM-yyyy").format(Date())
        val fechaObtenida = myFunctions.getCustomDate()
        assertEquals(fechaEsperada, fechaObtenida)
    }
}
```

Las anotaciones se utilizan para indicar que es cada método de la clase, ya que si no se indica mediante las anotaciones de JUnit, son simples métodos dentro de una clase.

Anotaciones comunes de JUnit:

| Anotacion | Descripcion |
| --- | --- |
| `@Test`| Indica que el método es un Test |
| `@Before`| El método se ejecutará antes de **cada** test. Si hay 3 tests se ejecuta 3 veces |
| `@After`| Igual que `@Before` pero después de cada test |
| `@BeforeClass` |Se ejecutará dicho método una vez antes de ejecutar los tests|
| `@AfterClass` | Se ejecutará el método al finalizar la ejecución de todos los tests|

Existen mas anotaciones pero las básicas son las de la tabla. 

Un punto importante es que las anotaciones `BeforeClass` y `AfterClass` deben anotar a métodos estáticos y públicos; en **java** con poner el método como `public static` funciona, en **kotlin** el método debe estar dentro de un `companion object` *(para que sea static)* y utilizar la anotación `@JvmStatic` aparte de `@BeforeClass` o `@AfterClass`.

Se puede observar que los nombres de las funciones en los tests pueden ir nombradas entre comillas inversas y con espacios en blanco, para que quede mas entendible.

1. El primer test utiliza la función `add` con dos parámetros a sumar y verifica que el resultado de la suma es el esperado.
1. El segundo, verifica la misma función pero que el resultado a comprobar no coincida con el esperado.
1. El ultimo verifica que la fecha devuelta por la función `getCustomDate` es igual que nuestro resultado esperado.


## Test de integración

Los tests de integración en la teoría parecen sencillos de entender, pero a mi, en la practica me han resultado mas complejos, ya que no me quedaba claro cuando se pasaba de pruebas unitarias a pruebas de integración.

Por ejemplo, si vamos a verificar que un **ViewModel** interactúa adecuadamente con una **base de datos** (como Room) o con una **API**, estaríamos realizando un test de integración.

Del mismo modo, si el **ViewModel** interactúa con un **repositorio** *(patron repository)*, y este repositorio a su vez se comunica con un **DataStore**, este escenario también seria un test de integración.

Pongamos el caso de testear la integración entre **ViewModel <--> API**, en esta prueba, lo que se pretende **verificar** es el **funcionamiento** entre los **dos componentes**. Es decir que el test seguirá siendo de integración si accedemos a los datos reales de la API o utilizamos un Mock.

1. Si accedemos a los datos reales de la API estamos testeando la integración real.
2. Si utilizamos un Mock para la API, estamos testeando como el ViewModel maneja los datos recibidos desde la API.

Otro ejemplo podría ser probar la clase que interactúa con la API. Si dicha clase utiliza el patrón Repository lo que vamos a testear es el repositorio de la API, entonces seria un test de integración ya que se verifica nuestro código con un componente externo. En cambio, si vamos a querer testear la lógica interna del repositorio, podríamos mockear los datos y esto pasaría a ser un test unitario.

En este ejemplo tenemos lo siguiente:

- Acceso a **base de datos** (Room).
	- El objeto que configura la base de datos.
	- El obtejo entity (referente a la tabla)
	- El DAO *(Data Access Objetc)* las consultas `sql`
 
 - La implementación al acceso a datos mediante el patrón Repository
	 - Un interface *(repository)*
	 - La implementación del interface 
  
- El **ViewModel** que utiliza el Repository para acceder a los datos.


![Diagrama de test de integracion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oi40g8wfksu5ykcrpd0k.png)

Y lo que vamos a testear es la integración entre el **ViewModel** y la **Base de Datos**.

#### ViewModel

```kotlin
@HiltViewModel
class UserViewModel @Inject  
constructor(/*Inyeccion Dependencias*/var userRepo: UserRepository) : ViewModel() {  
  
    private val _user = MutableStateFlow<UserEntity?>(null)  
    val user: StateFlow<UserEntity?> = _user  
  
    fun getUser(id: Int) {  
		
		// Ejecutamos una corutina en el scope del ViewModel
        viewModelScope.launch {  

			// Utilizamos el método getUser del Repository
            userRepo.getUser(id).collect { user ->  
                _user.value = user  
  
            }  
        }  
    }  
  
    fun saveUser(newUser: UserEntity) {  
        viewModelScope.launch {  
            userRepo.saveUser(newUser)  
        }  
  
    }  
}
```

#### Acceso a datos 

##### DB

> *La definición de la base de datos, especificando la tabla (entity) y las consultas (el DAO como abstract fun)*

```kotlin
@Database(entities = [UserEntity::class], version = 1)  
abstract class UsersDb : RoomDatabase() {  
  
    abstract fun usersDao(): UserDao  
  
    companion object {  
  
        private var instance: UsersDb? = null  
        fun getInstance(ctx: Context): UsersDb {  
            if (instance == null) {  
                instance =  
                    Room.databaseBuilder(ctx.applicationContext, UsersDb::class.java, "users_db")  
                        .build()  
            }  
  
            return instance!!  
        }  
    }  
}
```

##### Entity (tabla)

> *La tabla con su schema*

```kotlin
@Entity(tableName = "users")  
data class UserEntity(  
    @PrimaryKey val id: Int,  
    @ColumnInfo(name = "name") val name: String,  
    @ColumnInfo(name = "mail") val mail: String  
)
```

##### DAO

> *Las consultas a la base de datos*

```kotlin
@Dao  
interface UserDao {  
  
    @Query("SELECT * FROM users WHERE id = :id")  
    fun getUser(id: Int): Flow<UserEntity>  
  
    @Insert(onConflict = OnConflictStrategy.REPLACE)  
    suspend fun saveUser(user: UserEntity)  
}
```

#### Implementación acceso a datos

##### Repository

```kotlin
interface UserRepository {

	// Declaración de los métodos
    fun getUser(id: Int): Flow<UserEntity>  
    suspend fun saveUser(user: UserEntity)  
}
```

##### Repository Implementation 
```kotlin
class UserImpl(private val userDao: UserDao) : UserRepository {  

	// Definición de los métodos
    override fun getUser(id: Int): Flow<UserEntity> {  
        return userDao.getUser(id)  
    }  
  
    override suspend fun saveUser(user: UserEntity) {  
        return userDao.saveUser(user)  
    }  
}
```

> Por no añadir más código y hacer la explicación las complicada he optado por omitir la Inyección de Dependencias. Saber que la realizo con Dagger Hilt y proveo las clases `UsersDb`, `UserDao` y `UserImpl`.

#### Tests 

```kotlin
class UserViewModelTest {  
    private lateinit var database: UsersDb  
    private lateinit var user: UserEntity  
    private lateinit var userDao: UserDao  
    private lateinit var userRepository: UserRepository  
  
    private lateinit var viewModel: UserViewModel  
  
    @Before  
    fun setUp() {  
        // Contexto de la app en modo Test  
        val context = InstrumentationRegistry.getInstrumentation().context  
  
        // Creamos la BBDD ficticia en RAM  
        database = Room.inMemoryDatabaseBuilder(context, UsersDb::class.java).build()
        
        userDao = database.usersDao()  
        userRepository = UserImpl(userDao)  
        viewModel = UserViewModel(userRepository)  
  
        user = UserEntity(1, "Alice", "alice@mail.com")  
    }  
  
    @After 
    fun tearDown() {
		// Este metodo se ejcutará después de cada test
        database.close()  
    }  
  
    @Test  
    fun shouldViewModelSaveUserInDB() = runBlocking {  
        // Al ser un metodo 'suspend' debemos utilizar 'runBlocking' para ejecutar la corutina y  
        // bloquear el hilo.  

        // El ViewModel guarda el usuario        
        viewModel.saveUser(user)  
		
        // Obtenemos usuario con id: 1
        val savedUser = userRepository.getUser(1).first()  
  
        // Verificamos que el usuario es el mismo que el de la base de datos.  
        // Si es el mismo, el viewmodel ha funcionado correctamente a la hora        
        // de guardar el usuario en bbdd.        
        assertEquals(user, savedUser)  
    }  
	
    @Test  
    fun saveUser() {  
        runBlocking {
	        // Probamos a guardar un usuario desde el ViewModel
            viewModel.saveUser(user)  

			// Obtenemos un usuario con id: 1
            val userFromDb = userDao.getUser(1).first()  
			
			// Verificamos que los usuarios son iguales
            assertEquals(userFromDb, user)  
        }  
    }  
}
```



## Test de IU

En este ejemplo testearé un botón en Jetpack Compose, para realizarlo lo haremos como en el ejemplo.

#### Composable

```kotlin
@Composable  
fun Greeting(name: String) {  
    var showDialog by remember { mutableStateOf(false) }  
    Button(onClick = { showDialog = !showDialog }  
    ) {
        Text(text = "Click me!")  
    }  
  
    if (showDialog) {  
        Log.d("Saludo","showDialog")  
        onClickDialog()  
    }}  
  
@Composable  
fun onClickDialog() {  
    Dialog(onDismissRequest = { /*TODO*/ }) {  
        Text(text = "Dialogo")  
    }
```

#### Test

```kotlin
class GreetingComposableTesting {  
  
    private val name = "TEST"  

	// Creamos una regla de JUnit para un Composable
    @get:Rule
    val rule = createComposeRule()  
  
    @Before
    fun setContent() {  
		// A la regla le añadimos el composable a testear
        rule.setContent { Greeting(name) }  
    }  
  
    @Test  
    fun buttonExists() {  
    // Comprobamos que el nodo con el texto "Click me!" existe.  
        rule  
            .onNodeWithText("Click me!")  
            .assertExists("No existe")  
    }  
  
    @Test
    fun clickButton() {  
        rule  
            .onNodeWithText("Click me!")  
			// Verificamos que el nodo es clickable
            .assertHasClickAction()  
			// Hacemos click en el botón
            .performClick()  
    }  
  
}
```


Adjunto cheatsheet de testing en Jetpack Compose:

![Jetpack Compose testing cheatsheet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zeogn0kk82pa8dfkvvb5.png)

# Referencias


{% embed https://developer.android.com/training/testing?hl=es-419 %}

{% embed https://developer.android.com/training/testing/fundamentals?hl=es-419 %}

{% embed https://developer.android.com/jetpack/compose/testing?hl=es-419 %}