En el desarrollo de aplicaciones móviles es muy común utilizar bases de datos para almacenar y gestionar la información que se utiliza en la aplicación. En el caso de Android, una de las opciones más populares es Room, una librería de persistencia de datos que proporciona una capa de abstracción sobre SQLite.
Una de las ventajas de Room es que ofrece soporte para el uso de datos reactivos, lo que permite a los desarrolladores trabajar con flujos de datos en lugar de con consultas síncronas. Esto hace que sea más sencillo y eficiente gestionar la información en tiempo real, ya que los cambios en la base de datos se reflejan de forma automática en los flujos de datos.
Sin embargo, durante el proceso de testing es conveniente poder simular la base de datos y no depender de una base de datos real. En este artículo veremos cómo podemos crear una clase “fake” que simule una base de datos reactiva utilizando Room y que podamos utilizar durante los tests.
Definición de la interfaz del DataSource
Lo primero que debemos hacer es definir una interfaz que represente nuestra fuente de datos local. En este caso, vamos a suponer que tenemos una aplicación de películas y que queremos almacenar una lista de películas en la base de datos. La interfaz podría quedar de la siguiente forma:
interface MovieLocalDataSource { val movies: Flow<List<Movie>> suspend fun isEmpty(): Boolean fun findById(id: Int): Flow<Movie> suspend fun save(movies: List<Movie>): Error? }
Como se puede ver, la interfaz cuenta con una propiedad movies
que es un Flow
de listas de películas, y tres métodos:
isEmpty
: un método suspendido que nos permite comprobar si la base de datos está vacía.findById
: un método que nos permite buscar una película por su identificador y que devuelve unFlow
de la película encontrada.save
: un método suspendido que nos permite guardar una lista de películas en la base de datos y que devuelve unError
en caso de que se produzca algún problema.
Implementación del DataSource con Room
Ahora vamos a implementar una clase que se encargue de conectarse a una base de datos Room y que implemente la interfaz MovieLocalDataSource
.
Vamos a suponer que tenemos una entidad MovieEntity
que representa a las películas en la base de datos y una clase MovieDao
que proporciona los métodos necesarios para realizar operaciones en la base de datos (como obtener todas las películas, insertar películas, etc).
Con esto en mente, la clase MovieRoomDataSource
podría quedar de la siguiente forma:
class MovieRoomDataSource @Inject constructor(private val movieDao: MovieDao) : MovieLocalDataSource { override val movies: Flow<List<Movie>> = movieDao.getAll().map { it.toDomainModel() } override suspend fun isEmpty(): Boolean = movieDao.movieCount() == 0 override fun findById(id: Int): Flow<Movie> = movieDao.findById(id).map { it.toDomainModel() } override suspend fun save(movies: List<Movie>): Error? = tryCall { movieDao.insertMovies(movies.fromDomainModel()) }.fold( ifLeft = { it }, ifRight = { null } ) }
Como se puede ver, en la implementación de la interfaz MovieLocalDataSource
se hace uso de la clase MovieDao
para acceder a la base de datos y realizar las operaciones correspondientes. Por ejemplo, para obtener todas las películas se utiliza el método getAll
de MovieDao
y para insertar películas se utiliza el método insertMovies
.
Implementación del DataSource Fake
Ahora que tenemos la implementación que se conecta a una base de datos Room, vamos a crear la clase “fake” que simulará la base de datos en memoria. Esta clase se llamará FakeLocalDataSource
y tendrá la siguiente implementación:
class FakeLocalDataSource : MovieLocalDataSource { val inMemoryMovies = MutableStateFlow<List<Movie>>(emptyList()) override val movies = inMemoryMovies override suspend fun isEmpty() = movies.value.isEmpty() override fun findById(id: Int): Flow<Movie> = movies .map { it.first { movie -> movie.id == id } } override suspend fun save(movies: List<Movie>): Error? { inMemoryMovies.value = movies return null } }
En esta clase se utiliza una propiedad llamada inMemoryMovies
que es un MutableStateFlow
con una lista de películas vacía. Esta propiedad es expuesta como el Flow
de películas de la interfaz a través de la propiedad movies
.
Cuando se desee añadir películas a la base de datos en memoria, se puede hacer a través del método save
de FakeLocalDataSource
, que recibe una lista de películas y las asigna a inMemoryMovies
. De esta forma, todos los observadores del Flow
movies
recibirán los nuevos datos.
Conclusión
Es importante destacar que, al implementar la interfaz MovieLocalDataSource
, ambas clases deben exponer un Flow
de películas y un método suspend
para comprobar si la base de datos está vacía, además de métodos para buscar una película por su identificador y guardar una lista de películas.
En el caso de MovieRoomDataSource
, estos métodos hacen uso de la clase MovieDao
para acceder a la base de datos Room y realizar las operaciones correspondientes.
En cambio, en FakeLocalDataSource
, estos métodos se implementan utilizando la propiedad inMemoryMovies
y sin hacer uso de una base de datos.