Como testear Interceptores de Retrofit – Android

Hace poco veíamos como testear nuestra API con Retrofit en Android. Quedaron cosas por ver, así que hoy continuamos con más testing en Android.

Retrofit es una excelente librería que nos facilita la labor de llamar a nuestras APIs, a la vez que nos facilita herramientas para que el control sobre los procesos sea absoluto.

Uno de los mecanismos que nos proporciona son los interceptores. Estos son una capa de procesado que, tal como el nombre indica, interceptan las peticiones y las transforman. Pueden capturar tanto a la salida como a la entrada de la petición.

En mi caso, el uso que suelo darle es añadir parámetros de forma genérica a mis peticiones, por ejemplo, el token de usuario. Otro suele ser verificar si mi petición devuelve un código 401 y así intentar hacer un refresco de token.

En nuestra aplicación de ejemplo ya tenemos un interceptor que se encarga de añadir el apiKey necesaria a todas las peticiones, ApiKeyInterceptor, hoy crearemos un test para esta parte.

En un vistazo rápido a nuestra clase, por un lado ya estoy viendo un problema, y es que la clase necesita como parámetro el Context, esto en test unitarios de Android es un problema, ya que nos obligaría a tener tests instrumentados, y es difícil de Mockear. Así que lo que haremos será sustituir a este por algo que sea más sencillo de sustituir.

Siguiendo con la explicación de esta clase, esta implementa Interceptor, y para ello debe sobreescribir el método intercept, que será donde capturaremos la petición y la transformaremos a nuestro gusto. En este caso, añadimos un parámetro queryString a nuestra petición.

Así que, básicamente, esto es lo que testearemos, que dada una petición de entrada, su url de salida debe incluir el parámetro «api_key».

Antes de empezar haremos el cambio del Context. Simplemente en el constructor de ApiKeyInterceptor, lo cambiamos por la clase Configuration, que ya nos da el apiKey. Y sustituimos.

class ApiKeyInterceptor(private val configuration: Configuration): Interceptor {

Y dentro de la clase buscamos la siguiente línea

val apiKey: String = context.resources.getString(R.string.apikey)

Y sustituimos por la siguiente

val apiKey: String = configuration.apiKey

En AppModule, donde se definen las dependencias, debemos añadir Configuration a la hora de crear MovieService, que es quien utiliza nuestro interceptor.

@Singleton @Provides
    fun provideMovieService(@ApplicationContext appContext: Context, configuration: Configuration): MovieService {

....

httpClient.addInterceptor(ApiKeyInterceptor(configuration))
....

A continuación, creamos nuestro archivo de test.

@RunWith(JUnit4::class)
class ApiKeyInterceptorTest {

Y la estrategia para probar en este caso será, al igual que el artículo anterior, crear un servidor Mock, en este caso, también haremos una interface Mock para crear una petición que podamos controlar, y verificar finalmente que la request se forma correctamente.

Así que para no hacer más largo de lo necesario esto, lo que haré será copiar y pegar la parte de la creación del servidor mock de nuestro test anterior.

    private lateinit var mockWebServer: MockWebServer
    private val mainThreadSurrogate = newSingleThreadContext("UI thread")

   
    @Before
    fun createService() {
        Dispatchers.setMain(mainThreadSurrogate)

        mockWebServer = MockWebServer()

        service = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(ApiResponseCallAdapterFactory())
            .build()
            .create(MovieService::class.java)

    }

    @After
    fun stopService() {
        mockWebServer.shutdown()

        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }

    private fun enqueueResponse(fileName: String) {
        val inputStream = javaClass.classLoader
            ?.getResourceAsStream("api-response/$fileName")?.source()?.buffer()

        mockWebServer.enqueue(
            MockResponse()
                .setBody(inputStream!!.readString(Charsets.UTF_8))
        )
    }

Ahora mismo tiene que fallar, porque service no está definido, e incluso no será de tipo MovieService. Así que vamos a arreglar eso.

Fuera de nuestra clase de test, crearemos una interface TestService, y definimos una petición GET. Devolverá un objeto ApiObjectTest, que también definiremos aquí.

data class ApiObjectTest(val data: String)

interface TestService {
    @GET("test_request")
    suspend fun testRequest(): ApiResponse<ApiObjectTest>
}

Una vez tenemos esto, acabamos de definir nuestro service. Definimos a nivel de clase un testService.

private lateinit var testService: TestService
Y en el método createService corregimos como lo definimos. Cambiamos el nombre y al final, debemos indicarle que nuestras peticiones están definidas en TestService.
testService = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(ApiResponseCallAdapterFactory())
            .build()
            .create(TestService::class.java)

Aquí nos falta otra cosa, que es añadir nuestro interceptor. En los tests anteriores lo eliminamos porque no queríamos que interfirieran en nuestros tests, pero en este caso, queremos ver que efectos tienen.

Así que otra vez más en createService, vamos a instanciar nuestro interceptor y añadirlo a testService. La forma de hacerlo es muy similar a como lo hacemos en AppModule.

        val httpClient = OkHttpClient.Builder()
        
        val interceptor = ApiKeyInterceptor(configuration)
        httpClient.addInterceptor(interceptor)

        testService = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(ApiResponseCallAdapterFactory())
            .client(httpClient.build())
            .build()
            .create(TestService::class.java)

Si tenéis esto ya integrado, os daréis cuenta que falla porque configuration no está definido, vamos a solucionarlo.

Por un lado lo definimos a nivel de clase

private lateinit var configuration: Configuration

Y después, en createService, creamos un mock. Además, le decimos que cuando pidamos el apiKey devuelva el valor que necesitemos. Esto último lo podemos hacer aquí, o si necesitamos valores diferentes en cada test, podemos hacerlo a nivel de cada test.

        configuration = Mockito.mock(Configuration::class.java)
        Mockito.`when`(configuration.apiKey).thenReturn("my_api_key")

Para acabar las preparaciones, vamos a crear un archivo «test.json» dentro de la carpeta api-response de resources.

Y será un json muy sencillo, que debe conformar la estructura de ApiObjectTest.

{
  "data": "Esta es mi respuesta mock"
}

Vale, ahora ya podemos empezar a testear. Creamos nuestro test, y como siempre que tenemos coroutines, lo creamos con runBlocking.

@Test
fun `Check if is added Query Parameter Api Key OK`() = runBlocking(Dispatchers.Main) {
        
} 

En nuestro test, empezamos indicando al servidor que respuesta debe devolver, en este caso, será nuestro archivo test.json

enqueueResponse("test.json")

A continuación, ejecutamos nuestra petición mock con testService.testRequest() y con nuestro servidor mock recogemos el resultado con takeRequest

val data = testService.testRequest()

val request = mockWebServer.takeRequest()

Y por último, verificamos que el path que nos devuelve request, es el correcto, con el parámetro queryString del api Key.

MatcherAssert.assertThat(request.path, CoreMatchers.`is`("/test_request?api_key=my_api_key"))

Y con esto podemos estar seguros de que nuestras peticiones siempre llevarán el parámetro api key.

Por último, voy a hacer rápidamente un segundo test, añadiendo un parámetro queryString a la petición.

En TestService añado una segunda petición

@GET("test_request_param")
    suspend fun testRequestParam(@Query("param1") param1: String): ApiResponse<ApiObjectTest>

Después copio y pego el primer test y hago los cambios necesario para llamar a esta segunda petición, quedando de la siguiente forma.

@Test
    fun `Check if is added Query Parameter With param Api Key OK`() = runBlocking(Dispatchers.Main) {
        enqueueResponse("test.json")

        val data = testService.testRequestParam("custom_param")

        val request = mockWebServer.takeRequest()

        MatcherAssert.assertThat(request.path, CoreMatchers.`is`("/test_request_param?param1=custom_param&api_key=my_api_key"))
    }

Y de esta forma, hemos añadido un caso más, el cual nos dará más seguridad.

Al principio he comentado que uso los interceptores para renovar el access token. Este artículo ya se acaba aquí, pero será algo que abordaremos con total seguridad.

Como testear nuestra API Retrofit en Android

Cuando utilizamos un API nos estamos conectando a un sistema externo. Este puede cambiar en cualquier momento y ser un punto de fricción importante.

Al final un API debería seguir un contrato, en el cual el equipo de desarrollo se compromete a devolver los datos estructurados de una determinada forma.

Hoy veremos como podemos desde el lado de nuestras aplicaciones verificar que dada una respuesta determinada de un API, nuestra aplicación la llama, recibe y procesa de una determinada forma.

https://youtu.be/dY6zRH04meA

Como hasta ahora, seguimos con nuestra aplicación de ejemplo, y antes de empezar a testear, veremos que es lo que queremos testear y como.

Dado que lo que nos interesa testear es como se van a pasar los datos del API a nuestra aplicación, vamos a testear las peticiones a nuestra API. Pero al ser test unitarios, no debemos depender de que nuestra API esté siempre disponible y por eso lo que haremos será simular un servidor que nos de en cada test la respuesta que necesitamos.

Empezamos creando el archivo para nuestro test al que llamaremos MovieServiceTest

En resources, además, crearemos una carpeta api-response, aquí crearemos archivos json que simularán la respuesta que mandaremos desde nuestro servidor mock. He creado un archivo para el listado de películas y otro para el detalle de una película. Y para tener un ejemplo de cada uno, lo que hago es, con Postman o alguna herramienta similar, hago una petición real al listado de películas, copio el json de la respuesta y la pego en el archivo popular_movies.json.
Volviendo a nuestro archivo de test, empezamos creando el servidor mock. Para ello definimos una propiedad en la clase MovieServiceTest para nuestro servidor, además, también creamos otra para la interface que vamos a testear, MovieService.
private lateinit var mockWebServer: MockWebServer
private lateinit var service: MovieService

Y en el Before del test inicializamos el servidor y MovieService. Para el caso de MovieService, se inicializa prácticamente igual que en el caso de uso real, salvo que la url base cambia, y no incluimos los interceptores que podamos tener para que no interfieran en el test (los interceptores los testearemos aparte).

    @Before
    fun createService() {
  

        mockWebServer = MockWebServer()

        service = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(ApiResponseCallAdapterFactory())
            .build()
            .create(MovieService::class.java)

    }

Al acabar el test, en el After, lo que haremos será apagar el servidor.

    @After
    fun stopService() {
        mockWebServer.shutdown()
    }

Para simular nuestras peticiones, haremos un método que lea un archivo dado, y le diga a nuestro servidor mock que lo sirva. Para ello, una vez obtenido el archivo json que queremos, lo devolverá nuestro mockWebServer en el body.

private fun enqueueResponse(fileName: String) {
        val inputStream = javaClass.classLoader
            ?.getResourceAsStream("api-response/$fileName")?.source()?.buffer()

        mockWebServer.enqueue(
            MockResponse()
                .setBody(inputStream!!.readString(Charsets.UTF_8))
        )
    }

Vamos ahora con nuestro test. Para ello creamos un método nuevo en la clase MockServiceTest, y dado que tenemos que probar una función suspendida, debemos ejecutar el test con runBlocking, de este modo.

    @Test
    fun `Load Popular Movies OK`() = runBlocking(Dispatchers.Main) {

Lo siguiente que haremos en nuestro test, es preparar los datos. Lo haremos con nuestra función para mockear respuestas, así que añadimos a nuestro test una llamada a enqueueResponse pasándole por parámetro el nombre del archivo json que necesitamos que se responda, en este caso «popular_movies.json».

    @Test
    fun `Load Popular Movies OK`() = runBlocking(Dispatchers.Main) {
        enqueueResponse("popular_movies.json")

Ahora, ejecutamos la función a testear y obtenemos los datos a verificar. En este caso testeamos el método popularMovies, en data tendremos la respuesta que verificaremos y con mockWebServer.takeRequest tendremos datos sobre la petición.

    @Test
    fun `Load Popular Movies OK`() = runBlocking(Dispatchers.Main) {
        enqueueResponse("popular_movies.json")

        val data = service.popularMovies()

        val request = mockWebServer.takeRequest()

        val result = (data as ApiSuccessResponse).body

Ahora añadimos una comprobación bastante simple, verificar que nuestra petición se hace a través de un método GET. Y ejecutamos el test.

MatcherAssert.assertThat(request.method, CoreMatchers.`is`("GET"))

Al ejecutar este test, veremos que falla, pero no lo hace por el test, si no por que no se está ejecutando en el hilo correcto. Esto va a ser una constante cuando ejecutemos test con coroutines. Para corregir esto necesitamos crear un hilo que se ejecutará en el hilo principal y asignarlo al test.

El hilo lo añadimos como propiedad de la clase.

private val mainThreadSurrogate = newSingleThreadContext("UI thread")

En el método createService, al principio, le decimos que el Main de Dispatchers es nuestro hilo.

    @Before
    fun createService() {
        Dispatchers.setMain(mainThreadSurrogate)

Y en el stopService lo reseteamos.

    @After
    fun stopService() {
        mockWebServer.shutdown()

        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }

Ahora volvemos a ejecutar el test, y debería pasar a verde. Seguimos añadiendo las comprobaciones que necesitemos al test. Estas son la que he añadido yo.

        MatcherAssert.assertThat(request.path, CoreMatchers.`is`("/3/movie/popular?page=1"))
        MatcherAssert.assertThat(request.method, CoreMatchers.`is`("GET"))

        MatcherAssert.assertThat(result.results!!.size, CoreMatchers.`is`(20))

        val firstMovie: ApiObjectMovie = result.results!![0]
        MatcherAssert.assertThat(firstMovie.id, CoreMatchers.`is`("616037"))
        MatcherAssert.assertThat(firstMovie.imdbId, CoreMatchers.`is`("imdb_test"))
        MatcherAssert.assertThat(firstMovie.originalTitle, CoreMatchers.`is`("Thor: Love and Thunder"))

En la primera parte, verifico que la petición se hizo con método GET y el path de la petición es el correcto. Se podrían verificar más cosas, por ejemplo, que la petición tiene las cabeceras que correspondan.

En la segunda parte de verificaciones, he testeado la respuesta, en concreto que se han serializado correctamente los datos. En especial me interesan datos como imdbId cuyo nombre en el json es imdb_id y en la clase ApiObjectMovie se serializa a imdbId, así:

@SerializedName("imdb_id")
var imdbId: String?

Validando que los datos se serializan correctamente, sobre todo en estos casos, nos evitará sustos en el futuro.

Por otro lado, para obtener los valores en si a validar, debemos revisar, en este caso concreto, el archivo «popular_movies.json». Como siempre, este archivo y los demás, los podréis encontrar en el repositorio.

Nos quedaría testear el método movie de MovieService, pero una vez hemos hecho uno, el resto es seguir el mismo guión.

Aún nos quedan cosas por testear relacionadas con el API, pero por hoy lo dejamos aquí.

Como implementar Inyección de Dependencias en Swift

Hasta ahora nos estábamos preocupando más bien poco sobre un tema que si se hace bien nos puede traer grandes beneficios. Este no es otro que aplicar el principio de Inversión de Dependencias.

Si nos vamos a la Wikipedia, en este artículo se explica en detalle y nos dice esto en concreto:

  1. Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones (p.ej., interfaces).
  2. Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deben depender de abstracciones.

Así explicado la mayoría nos quedamos con cara de no entender nada. Pero si lo vemos con un ejemplo se entiende mejor.

En nuestra aplicación de ejemplo, en concreto en nuestro HomeViewModel, instanciamos un clase Configuration y otra ApiRestClient. Cuando empezamos a programar, esto suele ser lo más normal, necesitamos una clase que nos ayuda con un determinado tema, creamos una nueva instancia dentro de la clase que necesitamos y seguimos desarrollando.

Esto nos va a traer algunos problemas que a la larga nos pesarán bastante si nuestro código crece y crece (que lo hará).

  1. Imaginaros que tenemos una clase que almacena un dato concreto. Al principio, para no complicar la app, lo que hacemos es almacenar en UserDefaults, en nuestra clase instanciamos UserDefaults directamente y almacenamos. Además en otras clases que necesitamos leer este dato, vamos instanciando y leyendo el dato. Pasado un tiempo, se decide almacenar en base de datos, porque ahora se escriben más datos y en UserDefaults tantos datos son difíciles de manejar. Ahora, todas esas clases donde se estaba leyendo directamente a UserDefaults están fuertemente acopladas a este Framework.
  2. Por otro lado, si intentáis testear una de estas clases, os daréis cuenta de que es difícil hacerlo. No podremos testear por separado la funcionalidad en si de cada clase, vamos a tener siempre a UserDefaults interfiriendo sobre unos tests que se supone, deberían ser unitarios.

Bien, para solucionar todo este embrollo, llega la Inyección de Dependencias que sería la forma de aplicar el principio de Inversión de Dependencias. Siguiendo con el ejemplo anterior, una forma de solucionarlo sería pasando por constructor a todas las clases donde se lee o escribe una actracción.

Podemos tener una interface o protocol que nos diga que tenemos que implementar un método para leer y otro para escribir, pero esto no nos marca o exige como debemos hacerlo. Por otro lado, nos creamos una clase que implementa esta interface y que lee/escribe de UserDefaults, y se la pasamos por constructor a las clases que necesitamos. Cada una de estas clases ahora ya no tiene, ni que instanciar a UserDefaults, ni saber como se lee de este, y cambiar a otro sistema de almacenamiento sería mucho más sencillo, ya que simplemente se puede cambiar en la clase que maneja ahora la lectura/escritura de UserDefaults.

Bueno, dejando de lado la teoría y volviendo a nuestro proyecto. Estoy viendo que a nuestro proyecto le está pasando esto mismo, estamos instanciando dentro de clases otras, y esto va a suponer un problema a futuro. Para resolver en iOS estoy utilizando Resolver, que es una librería que nos ayudará con esto, pero revisando la documentación, me he dado cuenta que el creador va a deprecar la librería, pero tiene una nueva versión Factory, que se basa en la anterior, y básicamente nos permitirá hacer lo mismo.

Vamos con esta última entonces. Lo primero que tendremos que hacer es añadir la librería. Para ello, en File -> Add Packages y en la ventana que aparece, copiamos la url del repositorio https://github.com/hmlongco/Factory

En mi caso, he tenido problemas al añadir la librería, he tenido que añadir y eliminar 2 o 3 veces hasta que XCode la añadió correctamente y reconoció. Si usáis CocoaPods en vuestro proyecto, las instrucciones para instalar las podéis encontrar en el repositorio del autor de la librería.

Una vez añadida la librería, vamos a identificar aquellas clases que se están llamando dentro de otras.

El primer caso es el que comentamos antes, la clase HomeViewModel. Dentro de esta se está instanciando a la clase Configuration y a ApiRestClient.

Crearemos ahora un archivo nuevo que llamaremos AppInjection, en donde definiremos como instanciar estas clases. En este archivo importamos Factory y creamos una extension de Container (es una clase de nuestra librería).

Empezamos definiendo como instanciar Configuration. Creamos una propiedad configurationService y con la ayuda de Factory le decimos como debe instanciar Configuration. En este caso no necesita nada en el constructor.

Llegamos al caso de ApiRestClient. En este caso, necesitamos pasar en el constructor una instancia de Configuration. En este caso llamamos a nuestra propiedad configurationService.

import Factory


extension Container {
    static let configurationService = Factory(scope: .singleton) {
        Configuration()
    }
    
    static let apiRestClientService = Factory(scope: .singleton) {
        ApiRestClient(configuration: configurationService())
    }
}

Fijaros además que le estamos indicando a Factory en el parámetro scope el valor singleton. Con esto lo que conseguiremos es usar siempre la misma instancia de Configuration o ApiRestClient. El comportamiento por defecto es crear siempre una nueva instancia. Si deseamos otro comportamiento diferente se podría, os recomiendo leer la documentación de la librería.

Una vez tenemos esto listo vamos a ir un paso más allá, y añadiremos a Container también los ViewModels. Dentro de la extension Container añadimos:

static let homeViewModel = Factory() {
        HomeViewModel()
    }
    
    static let movieViewModel = Factory() {
        MovieViewModel()
    }

En este caso, no indicamos el scope, ya que queremos una nueva instancia de nuestros viewModel.

Una vez definido como se instancian nuestras clases, vamos a ver como utilizarlo.

Vamos a ViewModel, y vemos que tenemos algo así:

class HomeViewModel: ObservableObject {
    
    private let apiRestClient: ApiRestClient
    private let configuration: Configuration
    
    init() {
        self.configuration = Configuration()
        self.apiRestClient = ApiRestClient(configuration: configuration)
    }

Como veis, estamos instanciando dentro de nuestra clase Configuration y ApiRestClient. Veamos como librarnos de esto.

@Injected(Container.apiRestClientService) private var apiRestClient: ApiRestClient
    
init() {
        
}

Factory nos proporciona un wrapper @Injected, que nos permitirá obtener una instancia de la clase que necesitamos, y de este modo podremos limpiar el constructor.

En el caso de la vista, en lugar de utilizar @Injected, lo haré de la siguiente forma, ya que he detectado que junto a @ObservedObject la librería no funciona bien.

@ObservedObject var viewModel: HomeViewModel = Container.homeViewModel()

Y de este modo Factory nos dará una instancia del ViewModel que necesitamos. Se podría instanciar directamente el ViewModel en este punto, pero bajemos un momento hasta la preview. Y revisemos como cambia.

#if DEBUG
struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        let _ = Container.homeViewModel.register { MockHomeViewModel() }
        HomeView()
    }
}
#endif

Antes de esto, estábamos instanciando la vista y el viewModel, y asignando el ViewModel a la vista. Ahora lo que se hace es decirle a nuestro gestor de inyección de dependencias que vamos a cambiar nuestro HomeViewModel por MockHomeViewModel cuando mostremos la preview.

Esta misma técnica la podremos aplicar a la hora de hacer tests (que aún no llegamos, pero lo haremos).

Como conclusión, hemos modificado la forma en la que se asignan Configuration, ApiRestClient o los ViewModel, pero no la funcionalidad, aún así, la potencia que nos dará esto a medida que crezca el código será brutal, además de facilitarnos cambiar en nuestro código una pieza por otra con mucha facilidad. Por último, controlar como se instancia un objeto, a partir de ahora, se hará en un único sitio.

iOS – Como leer datos desde un WS con Alamofire

Cuando desarrollamos una app para móviles, más tarde o temprano vamos a conectarnos a alguna fuente de datos externa, normalmente un API. Y para ello, normalmente, utilizamos alguna librería, la más común en iOS suele ser Alamofire.

Hoy seguiremos con nuestra app para películas, y lo que haremos será leer datos de un web service proporcionado por un API.

Antes de ponernos en faena, tengo que hacer dos trabajos previos. El primero será crear una Vista para el detalle de una película. Así que me crearé un archivo MovieView, y siguiendo con nuestra arquitectura MVVM, también crearé un archivo MovieViewModel.

En el segundo trabajo previo me voy a anticipar a lo que va a pasar cuando llamemos a nuestra API desde el ViewModel y crearé Mocks de nuestros ViewModel que llamen datos mock cuando se pintan las previews de nuestras vistas. De este modo, nuestras previews no dependen de fuentes de datos reales, que más adelante se puede llegar a complicar.

Veamos como se hace esto en la pantalla de HomeView (en la de MovieView será igual, podréis revisarlo en el repositorio).

Primero creamos en el archivo HomeViewModel una clase que extienda de HomeViewModel. Lo creamos dentro de un bloque if DEBUG para que a la hora de publicar la aplicación, este código no se llegue a ejecutar.

#if DEBUG
class MockHomeViewModel: HomeViewModel {
    
    override func loadMovies() {
        self.movies = moviesTest
    }

}
#endif

Ahora en nuestra vista HomeView haremos un par de cambios en nuestra preview.

Metemos nuestra vista en una variable, instanciamos nuestro nuevo ViewModel mock y se lo asignamos, y finalmente retornamos la vista. Acordaros de incluirlo en un if DEBUG, si no, a la hora de publicar os dará error por utilizar el ViewModel que si que está dentro de un if DEBUG.

#if DEBUG
struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        var view = HomeView()
        
        let viewModel = MockHomeViewModel()
        view.viewModel = viewModel
        
        return view
    }
}
#endif

De este modo, cuando se pinten nuestra vistas en modo preview se usarán los datos mock que hemos definido. Así, tendremos en todo momento controlado lo que queremos mostrar en cada preview.

Vamos ahora si con la parte que interesa, llamar a datos desde una API.

Empezaremos leyendo el listado de películas, para ello, con una herramienta tipo Postman podemos ver la estructura de la respuesta, y como debemos hacer la petición.

Aquí os dejo la petición y un ejemplo de resultado en formato json de la petición al listado de películas. Acordaros de utilizar vuestra ApiKey.

https://api.themoviedb.org/3/movie/popular?api_key=HERE_YOUR_API_KEY

{
    "page": 1,
    "results": [
     {
            "adult": false,
            "backdrop_path": "/7ZO9yoEU2fAHKhmJWfAc2QIPWJg.jpg",
            "genre_ids": [
                28,
                878,
                53
            ],
            "id": 766507,
            "original_language": "en",
            "original_title": "Prey",
            "overview": "When danger threatens her camp, the fierce and highly skilled Comanche warrior Naru sets out to protect her people. But the prey she stalks turns out to be a highly evolved alien predator with a technically advanced arsenal.",
            "popularity": 7796.888,
            "poster_path": "/ujr5pztc1oitbe7ViMUOilFaJ7s.jpg",
            "release_date": "2022-08-02",
            "title": "Prey",
            "video": false,
            "vote_average": 8.1,
            "vote_count": 2730
        }
],
    "total_pages": 34792,
    "total_results": 695830
}

Vemos que tenemos un objeto principal que nos da datos de paginación del listado, y en la propiedad results tendremos un listado de objetos con datos de películas.

Cuando leemos con Alamofire un API lo que se suele hacer es convertir el resultado, un archivo json, a estructuras definidas en nuestra app. Para ello hacemos uso de Codable que nos permite definir estas estructuras en Swift.

Vamos a definir estos objetos. Primero, vamos con los datos en si de una película. Para hacerlo sencillo definiremos unas pocas propiedades. Así también veremos que no es necesario dar de alta todas las propiedades, solo las que nos interesan. Así que nos vamos a quedar de momento solo con 4 propiedades.

{
            "id": 766507,
            "overview": "When danger threatens ...",
            "poster_path": "/ujr5pztc1oitbe7ViMUOilFaJ7s.jpg",
            "title": "Prey"
}

Y pasándolo a una estructura en Swift nos quedará así. En un principio, y por simplicidad no hay ninguna propiedad Optional, pero si nuestra API nos devuelve algún null, solamente debemos marcar la que corresponde como Optional.

struct ApiObjectMovie: Codable {
    var id: Int64
    var original_title: String
    var overview: String
    var poster_path: String
}

Personalmente, y debido a mi experiencia previa, siempre defino como Optional todas las propiedades de mis objetos de API, esto es porque como es un elemento externo, no puedo asegurar si va a ser nulo o no. Da un poco más de trabajo, pero evitamos errores.

Seguimos con la siguiente estructura, la que corresponde al paginador, así que nuevo archivo, y veámoslo en detalle.

struct ApiObjectPaginator<T: Codable>: Codable {
    var page: Int
    var total_pages: Int
    var total_results: Int64
    var results: [T]
}

Aquí vemos cuatro propiedades, las tres primeras no tienen dificultad, mapean datos de tipo Int. Así que nos centraremos en la última «results». Si nos fijamos, es un array de objetos tipo «T». Pero que es esto? Si no hemos definido ningún struct o class que se llame así. Esta es una forma muy sencilla y potente de utilizar clases genéricas y no casarnos con una clase concreta. De hecho, se llama «generic».

Simplemente, al definir nuestra struct (o class), después del nombre, entre los símbolos de < y >, le indicamos que estaremos usando un genérico al que llamaremos T y además, en este caso, debe implementar Codable, en caso contrario dará error.

Y como se usa? Pues muy sencillo, cuando queramos definir un paginador, simplemente lo haremos así:

ApiObjectPaginator<ApiObjectMovie> 

De este modo, estaremos diciendo que tenemos un paginador con un listado results de tipo ApiObjectMovie. Esto finalmente nos permitirá reusar esta clase, y no tener que crear una nueva para cada listado paginado. Sigamos.

Ahora que ya tenemos nuestras struct para los objetos del API definidos, veamos como hacer la petición con Alamofire.

Creamos un archivo ApiService y dentro de este empezamos con la clase desde donde haremos las peticiones.

class ApiRestClient {
    private let urlBase: String = "https://api.themoviedb.org/3/movie/"
    private let APIKEY_NAME: String = "api_key"
    private let configuration: Configuration
    
    public init(configuration: Configuration) {
        self.configuration = configuration
    }
}

Definimos varias propiedades que necesitaremos más adelante, además le pasamos por el constructor el objeto Configuration que ya vimos en otro artículo. Este se encargará de darnos el valor correcto de nuestra Api Key.

Ahora definimos un par de estructuras para los errores.

struct ApiRestError: Error {
  let error: Error
  let serverError: ServerError?
}

struct ServerError: Codable, Error {
    var status: String
    var message: String
}

struct ConfigError: Error {
    var code: Int
    var message: String
}

Por un lado tenemos ApiRestError, que nos dará información en caso de que algo falle. Aquí tenemos un error para el cliente, por ejemplo si no tenemos conexión, y otro para errores que nos devuelva el API, por ejemplo si tenemos error 500.

Siguiente, creamos un protocolo donde daremos de alta cada una de las peticiones al API. En este caso, vamos a dar de alta dos, una para el listado y otra para leer el detalle de una película.

protocol ApiServiceProtocol {
    
    func fetchPopularMovies() -> AnyPublisher<DataResponse<ApiObjectPaginator<ApiObjectMovie>, ApiRestError>, Never>
    
    func fetchMovie(movieId: String) -> AnyPublisher<DataResponse<ApiObjectMovie, ApiRestError>, Never>
    
}

Como veis, nuestros métodos, deben devolver un Publisher, esto forma parte de Combine, el cual es una estructura que nos permitirá observar el resultado de la petición, y a la cuales nos suscribiremos finalmente desde nuestros ViewModel, pero eso lo veremos más adelante.

Ahora vamos a ver como implementar la petición para el listado de películas. Creamos una extensión de ApiRestClient que extienda de ApiServiceProtocol.

extension ApiRestClient: ApiServiceProtocol {

    func fetchPopularMovies() -> AnyPublisher<DataResponse<ApiObjectPaginator<ApiObjectMovie>, ApiRestError>, Never> {
        let url = URL(string: "\(urlBase)popular?\(APIKEY_NAME)=\(configuration.apiKey ?? "")")!
        
        return AF.request(url, method: .get)
            .validate()
            .publishDecodable(type: ApiObjectPaginator<ApiObjectMovie>.self)
            .map { response in
                response.mapError { error in
                    let serverError = response.data.flatMap { try? JSONDecoder().decode(ServerError.self, from: $0)}
                    return ApiRestError(error: error, serverError: serverError)
                    
                }
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
}

Creamos primero la url de nuestra petición. Estamos asumiendo que se creará bien, luego mejoraremos esta parte. Seguidamente llamamos con Alamofire a request, pasándole la url y diciéndole que se trata de un get.

Importante, debemos validar además la respuesta con validate, de otro modo, Alamofire intentará decodificar la respuesta a la struct que le indicamos sin saber si la petición fue o no exitosa.

Con la ayuda de mapError revisaremos si tenemos algún error, y en caso afirmativo crearemos el error correspondiente.

Por último indicamos en que hilo vamos a publicar el resultado y creamos nuestro publisher.

El código para leer una película es muy similar, solamente debemos cambiar como se construye la URL y en el método publishDecodable indicar que tipo de struct esperamos.

.publishDecodable(type: ApiObjectMovie.self)

De hecho, al ser tan parecido, después veremos como mejorar esta parte.

Vamos primero hasta nuestro ViewModel y veamos como utilizar todo esto.

Empezamos instanciando todo lo que necesitamos. En este punto no me meteré a usar inyección de dependencias para no alargar demasiado, pero en el futuro trataremos este punto.

let apiRestClient: ApiRestClient
let configuration: Configuration
private var cancellableSet: Set<AnyCancellable> = []
    
init() {
    self.configuration = Configuration()
    self.apiRestClient = ApiRestClient(configuration: configuration)
}

Y ahora en loadMovies, cambiamos nuestra fuente de datos. Hasta ahora estábamos leyendo datos mock, pero ahora vamos a leer datos reales.

func loadMovies() {
    apiRestClient.fetchPopularMovies()
       .sink { (dataResponse) in
            if dataResponse.error != nil {
                print(dataResponse.error.debugDescription)
            } else {
                self.movies = dataResponse.value?.results.compactMap({ apiItem in
                        Movie(id: String(apiItem.id),
                         imdbId: nil,
                              title: apiItem.original_title,
                              overView: apiItem.overview,
                              posterPath: apiItem.poster_path)
                    }) ?? []
                }
        }
        .store(in: &cancellableSet)
    }

Con nuestra clase apiRestClient llamamos al método que nos interesa, listado de películas en este caso. Con sink podemos ir recibiendo los valores que nos llegan desde nuestro Publisher.

Aquí revisamos si la respuesta es un error o no, y en caso de no serlo, lo que haremos será convertir con compactMap cada uno de los objetos ApiObjectMovie a uno Movie, que es lo que espera recibir nuestra vista.

Y finalmente el resultado se asigna a nuestro listado de pel´ículas movies que ya se estaba observando desde la vista.

Y hasta aquí, si compilamos y ejecutamos, estaremos leyendo nuestro listado de películas. Pero… hay un par de puntos que dijimos que se podían mejorar.

El primero es como se crea la url para cada petición en AiRestClient.

Estábamos haciendo esto. Lo cual podría fallar.

let url = URL(string: "\(urlBase)popular?\(APIKEY_NAME)=\(configuration.apiKey ?? "")")!

Así que vamos a verificar con un guard que estos valores no sean nulos.

guard let apiKey = configuration.apiKey,
              let url = URL(string: "\(urlBase)popular?\(APIKEY_NAME)=\(apiKey)") else {
            
            return emptyPublisher(error: ConfigError(code: 555, message: "No URL defined"))
        }

Esto nos obliga a devolver en el else de nuestro guard un Publisher, así que crearemos un método que nos ayude con esto.

func emptyPublisher<T: Codable>(error: ConfigError) -> AnyPublisher<DataResponse<T, ApiRestError>, Never> {
        
        return Just<DataResponse<T, ApiRestError>>(
            DataResponse(request: nil,
                         response: nil,
                         data: nil,
                         metrics: nil,
                         serializationDuration: 0,
                         result: .failure(ApiRestError(error: error, serverError: nil))
                        )
        )
        .eraseToAnyPublisher()
        
    }

Y debemos crear un nuevo tipo de error. Con la propiedad code podremos verificar de que tipo de error personalizado se trata.

struct ConfigError: Error {
    var code: Int
    var message: String
}

El siguiente punto a mejorar es la parte de la petición a Alamofire que estamos repitiendo.

Para ello nos llevamos toda esto a un nuevo método que extienda de DataRequest, y gracias a los generics podremos hacer que reciba cualquier tipo de struct que implemente Codable.

extension DataRequest {
    
    func proccessResponse<T: Codable>(type: T.Type) -> AnyPublisher<DataResponse<T, ApiRestError>, Never> {
        self
            .validate()
            .publishDecodable(type: type.self)
            .map { response in
                response.mapError { error in
                    let serverError = response.data.flatMap { try? JSONDecoder().decode(ServerError.self, from: $0)}
                    return ApiRestError(error: error, serverError: serverError)
                    
                }
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
}

Y desde cada uno de los métodos que hace una petición con Alamofire debemos hacer lo siguiente.

return AF.request(url, method: .get)
            .proccessResponse(type: ApiObjectPaginator<ApiObjectMovie>.self)

Como veis, en un par de líneas ahora podemos hacer una petición, y centralizamos la forma en la que procesamos la respuesta.

Y con esto ya hemos empezado a leer datos reales desde un API. Si recordáis, nuestra aplicación, además, debe almacenar estos datos en una base de datos local, para tener de respaldo cuando no tengamos conectividad. Pero eso lo veremos en otro artículo.

Finalmente, os dejo el enlace al repositorio como siempre donde podréis revisar también como implementé la llamada para leer el detalle de una película https://github.com/3pies/moviesios

Como guardar configuraciones con Swift y iOS

La siguiente parte que quería abordar era como hacer peticiones a una API con Alamofire, pero cuando empecé me di cuenta que no quería exponer mi Api Key en el repositorio donde estoy subiendo el código de la aplicación.

Así que lo que vamos a hacer antes de llamar a nuestra API, será ver como almacenar correctamente nuestra Api Key de forma segura.

Lo primero que haremos será crear un nuevo archivo de tipo «Configuration Settings File» donde podremos incluir todos los valores sensibles.

Creo una entrada para mi api key, así:

Lo siguiente que haremos será ir a nuestro archivo Info.plist, que para los proyectos nuevos hechos con SwiftUI no existirá. En este caso, tenemos que ir en nuestro proyecto a la pestaña Info, y dentro de «Custom iOS Target Properties» añadir una clave nueva (pulsamos sobre la última y después Enter). Notaremos que después de hacer esto tenemos un nuevo archivo Info.plist.

Tenemos que añadir un nuevo par clave-valor. La key será «API_KEY» y el valor «$(API_KEY). Esta última es la que hace referencia a nuestro archivo de configuración del paso anterior.

Ahora, en nuestro proyecto, le decimos que archivo de configuración debe tomar. Podemos tener uno diferente para cada tipo de compilación si lo queremos. En este caso usaremos el mismo.

¿Cuándo podemos tener varios? Imaginaros que una de las claves fuese una url a un servidor. Para debug podría apuntar a un servidor de desarrollo y para release al de producción.

En nuestro archivo .gitignore debemos añadir una entrada para que no añada los cambios de nuestro archivo de configuración. Este archivo lo abro desde terminal con vim, pero aquí el editor es lo de menos.

Una vez ya tenemos todo configurado, vamos a utilizarlo en nuestra app.

Primero de todo creamos un archivo Configuration.swift. Aquí expondremos una propiedad con el valor de nuestra api key. Esto lo estaremos leyendo de nuestro archivo Info.plist.

class Configuration {
    
    let apiKey: String? = Bundle.main.infoDictionary?["API_KEY"] as? String
    
}

Ahora solo quedará instanciar nuestra clase Configuration donde se necesite este api key.

let configuration = Configuration()
configuration.apiKey

Como veis, es muy sencillo manejar nuestras api keys o cualquier otro parámetro de configuración.

Por supuesto, y por seguridad, no incluyáis claves en vuestra aplicación, en todo caso, si las necesitáis, tenedlas a buen recaudo en un servidor seguro, y solo obtenedlas bajo una conexión segura.

Podéis ver el código completo en el repositorio donde estamos haciendo nuestra aplicación para leer películas https://github.com/3pies/moviesios o podéis ver el proceso completo en mi canal de Youtube.

Android – Como testear nuestra base de datos

Hace unas semanas dejábamos lista nuestra aplicación de ejemplo para testear. Ya tenía una funcionalidad mínima y faltaban los tests. Si recordáis, nuestra aplicación se conecta a un API, descarga datos y los almacena en base de datos local.

Hoy vamos a testear nuestra base de datos. Ya veréis como es muy sencillo. Vamos allá!

Antes de testear, empezaré explicando muy rápido esta parte. Las bases de datos locales nos ayudan a persistir datos de tal forma que no sea necesaria una conexión a internet ininterrumpida. En el caso de nuestra aplicación, estamos usando la librería Room de Android, la cual es muy potente, y con pocas líneas podemos persistir nuestros datos.

En la carpeta «db» de nuestro proyecto tenemos 2 archivos.

MovieDb es la clase que gestiona nuestra base de datos. A través de anotaciones indicaremos varios parámetros, por un lado las entidades y por otro la versión de nuestra base de datos. Debemos acordarnos de subir esta versión a medida que se introduzcan cambios en esta.

Además, definimos los DAOs que manejará. Vamos a ver este, el cual se encuentra en MovieDao. Como en nuestra base de datos, debemos marcarlo con una anotación, @Dao, para indicar que se trata de un Dao y nuestra librería Room lo reconozca como tal.

También debemos definir las entidades o tablas de nuestro modelo de datos, para ellos, en la carpeta model del proyecto tenemos una entidad Movie. Veámosla.

Ya veis que es muy sencillo, indicamos con una anotación, @Entity, que se trata de una entidad, e indicamos el nombre de las propiedades que forman parte de la primary key. En este caso solo tenemos una pero pueden ser varias.

Y definimos los campos que queremos tener en nuestra entidad como propiedades.

Por último, para inicializar todo esto y empezar a utilizarlo, en nuestra carpeta di, en AppModule, y con la ayuda de Hilt+Dagger, vamos a inicializar nuestra base de datos.

No quiero entrar en detalle con el tema de Hilt+Dagger, ya que haré un artículo más profundo sobre esto, pero basicamente nos ayudará a instanciar las clases que necesitamos a lo largo de la aplicación, y de este modo usar el principio de inyección de dependencias.

Volviendo a como instanciamos nuestra base de datos, por un lado instanciamos nuestra base de datos, dándole un nombre, indicando la clase MovieDb para indicarle que esta es nuestra base de datos.

Además, le decimos con fallbackToDestructiveMigration que cuando tengamos una nueva versión, elimine la versión actual y cree una nueva. Opción interesante si de momento no nos planteamos un sistema de migración.

También instanciamos los DAOs por si queremos acceder directamente a ellos, pero teniendo acceso a la clase de base de datos no nos haría falta.

Bien, si has llegado hasta aquí, ya hemos dado un repaso rápido a la parte que gestiona la base de datos. Vamos ahora a testear nuestra base de datos.

Lo primero que hay que saber es que los tests de base de datos son instrumentados, ¿esto que quiere decir? que los tests se deben ejecutar o en dispositivo físico o en emulador. Este tipo de tests se deben crear dentro de la carpeta que tiene el namespace y entre paréntesis androidTest.

De todo lo que hemos ido viendo vamos a testear nuestros DAOs, de esta forma podremos comprobar que nuestras consultas devuelven los resultados que se esperan de ellas.

Para poder probar esto, lo que haremos será crear una versión de nuestra base de datos que en lugar almacenar en base de datos local, lo hará en memoria. Además, haremos que las peticiones se hagan en el hilo principal de ejecución, de esta forma nos evitaremos efectos colaterales.

Veamos como será esta base de datos de pruebas:

Para empezar tendremos un objeto (db) de tipo MovieDb, en el cual almacenaremos los datos en memoria, y el cual se inicializará antes de cada test.

Al finalizar cada test cerraremos nuestra base de datos. De esta forma siempre tendremos una base de datos vacía, lo cual hará que los datos de un test no interfieran con los datos de otro.

Por último, también creamos un hilo en el cual ocurrirán todas las ejecuciones del test.

abstract class MovieDbTest {

    @OptIn(DelicateCoroutinesApi::class)
    private val mainThreadSurrogate = newSingleThreadContext("Main Thread")

    private lateinit var _db: MovieDb

    val db: MovieDb
        get() = _db

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun initDb() {
        Dispatchers.setMain(mainThreadSurrogate)
        _db = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            MovieDb::class.java
        ).build()
    }

    @After
    fun closeDb() {
        _db.close()
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }
}

Vamos ahora a testear nuestro DAO, para ello creamos una nueva clase MovieDaoTest que heredará la clase anterior, MovieDbTest. Y que ya será, por fin, nuestro test. Para ello, debemos indicar una anotación, @RunWith, para indicar que será un test.

@RunWith(AndroidJUnit4::class)
class MovieDaoTest: MovieDbTest() {

}

En MovieDao el primer método que tenemos es el de inserción. Comprobemos que se insertan bien los datos.

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(movies: List<Movie>)

Para ello, creamos un método para testear. Lo marcamos con la anotación @Test, e importante, al utilizar coroutines, debemos bloquear la ejecución y enviarla al hilo principal con runBlocking.

@Test
fun when_insert_load_same_data() = runBlocking(Dispatchers.Main) {

}

Para poder insertar, primero definiremos un set de datos. En este caso, un listado de Movie.

val list = listOf(
            Movie(id = "01", imdbId = "imdb01", title = "title_01", originalTitle = "original_title_01",
                originalLang = "en", overview = "overview_01", voteAverage = 4.3f, voteCount = 1000L,
                posterPath = null, releaseDate = "release_date_01", popularity = 5f,
                releaseDateTimestamp = 12132313132L),
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )

Seguidamente, ejecutamos el método a testear

db.movieDao().insert(list)

Vale, una vez tenemos en nuestra base de datos en memoria nuestros datos, la forma más sencilla para verificar si se insertó es leyendo de nuevo los datos. Después comprobamos que aparezcan los registros que insertamos.

val loaded = db.movieDao().loadMovies().take(1).toList()[0]
assertThat(loaded.size, CoreMatchers.`is`(2))

Utilizamos el mismo método loadMovies de nuestro DAO, que nos leerá todas nuestras películas. Al devolver este método un objeto tipo Flow, debemos tratarlo con el método take, y convertir a lista para finalmente tener la lista que necesitamos para verificar.

Vamos a crear otro test, esta vez del tirón. En este caso, si volvemos a nuestro archivo MovieDao, y nos fijamos otra vez en el método insert, vemos que tiene una anotación @Insert, en la cual se ha configurado que al insertar un registro con una clave duplicada se haga un Replace. Testeemos si esto se cumple.

@Test
    fun when_insert_repeated_replace_data() = runBlocking(Dispatchers.Main) {

        val list = listOf(
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )

        // Método a testear
        db.movieDao().insert(list)

        val loaded01 = db.movieDao().loadMovies().take(1).toList()[0]
        assertThat(loaded01.size, CoreMatchers.`is`(1))

        assertThat(loaded01[0].imdbId, CoreMatchers.`is`("imdb02"))

        val list2 = listOf(Movie(id = "02", imdbId = "imdb02_new", title = "title_02b", originalTitle = "original_title_02b",
            originalLang = "en", overview = "overview_02b", voteAverage = 2.8f, voteCount = 900L,
            posterPath = null, releaseDate = "release_date_02b", popularity = 4.5f,
            releaseDateTimestamp = 12132313132L),)

        db.movieDao().insert(list2)

        val loaded02 = db.movieDao().loadMovies().take(1).toList()[0]
        assertThat(loaded02.size, CoreMatchers.`is`(1))

        assertThat(loaded02[0].imdbId, CoreMatchers.`is`("imdb02_new"))

    }

Empezamos muy parecido, creamos un listado con un solo registro, lo insertamos, leemos y verificamos que se ha insertado un registro. Además, tomamos el primer registro que leemos, lo cual nos dará un objeto Movie, y verificamos el valor de la propiedad imdbId.

assertThat(loaded01[0].imdbId, CoreMatchers.`is`("imdb02"))

Ahora volvemos a insertar otro registro, el cual debe tener el mismo valor para la primary key. Leemos otra vez y comprobamos que seguimos teniendo un único registro.

Además, verificamos el valor de la propiedad imdbId otra vez, el cual ahora debe haber cambiado.

Por último, vamos a testear el método que lee un registro concreto. Para ello, hacemos una nueva función de test.

@Test
    fun when_load_on_movie_get_correct_entry() = runBlocking(Dispatchers.Main) {
        val list = listOf(
            Movie(id = "01", imdbId = "imdb01", title = "title_01", originalTitle = "original_title_01",
                originalLang = "en", overview = "overview_01", voteAverage = 4.3f, voteCount = 1000L,
                posterPath = null, releaseDate = "release_date_01", popularity = 5f,
                releaseDateTimestamp = 12132313132L),
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )


        db.movieDao().insert(list)

        val loaded = db.movieDao().loadMovie("02").take(1).toList()[0]
        assertThat(loaded.id, CoreMatchers.`is`("02"))
        assertThat(loaded.imdbId, CoreMatchers.`is`("imdb02"))
    }

En este caso, volvemos a crear un listado con datos y los insertamos. Leemos los datos utilizando el método que estamos testeando, loadMovie, y le decimos que lea el registro cuyo valor sea «02».

Una vez tenemos el objeto, verificamos que los valores de sus propiedades coincidan con lo que se espera.

En este caso concreto se está testeando una consulta muy sencilla, pero imaginad cuando tengáis consultas más complicadas, con varios joins o parámetros en el where. La tranquilidad y seguridad que os pueden dar este tipo de test no tiene precio.

Por último, comentaros que he tenido que añadir y eliminar algunas dependencias del proyecto que tengo en github donde voy poniendo el código. Revisar los cambios para saber cuales son las nuevas dependencias.

También os recuerdo que he grabado una sesión en Youtube hablando de este tema, revisarlo, suscribiros y no olvidéis darle un like si queréis que haga más videos testeando aplicaciones.

Código del repositorio https://github.com/3pies/moviesios

Como crear nuestro primer MVVM en iOS

Empezar a crear una aplicación desde cero siempre es excitante y a la vez abrumador. Demasiadas cosas que hacer en muy poco tiempo. Así que es importante pararse un momento y reflexionar como queremos hacerlo.

Un buen punto de inicio es pensar en una funcionalidad que nos permita definir a pequeña escala como queremos que sea la arquitectura de nuestra app, y a partir de ahí empezar a contruir.

En nuestro caso, una app que lea un listado y después vea un detalle nos ayudará a comprender y modelar nuestra app a pequeña escala.

Voy a utilizar MVVM como arquitectura, ya que nos ayudará a separar la parte vista del resto de la app. En esta arquitectura tenemos 3 partes, la parte View que es la que muestra la interface de usuario, la parte Model que es la que maneja los datos, y por último, la parte ViewModel, que por un lado se encarga de tratar los datos y por otro lado, pasa estos datos a la vista a través de databinding.

Vamos a empezar, voy a crear una vista muy sencilla, con su viewmodel y datos mock que nos permitirá tener un primer punto de arranque con estas 3 capas. Empecemos!!!

Model

Empezaré creando el modelo de datos, en este caso al tratarse de un listado de películas, haré un entidad llamada Movie, la cual será un struct plano y cuyo único cometido será almacenar los datos a mostrar.

Además, debe implementar el protocolo Identifiable, el cual nos ayudará después a la hora de iterar sobre un listado de Movie.

struct Movie: Identifiable {
    var id: String
    var imdbId: String?
    var title: String
    var overView: String
    var posterPath: String?  
}

Además, dado que no vamos a conectarnos ni con un API ni con base de datos local de momento, crearemos un array de tipo Movie con datos mock. Este nos quedará después para ayudarnos a crear las previews de nuestras vistas.

let moviesTest: [Movie] = [
    Movie(id: "1", imdbId: "imdb01", title: "Regreso al Futuro", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "2", imdbId: "imdb02", title: "Regreso al Futuro II", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "3", imdbId: "imdb03", title: "Regreso al Futuro III", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "4", imdbId: "imdb04", title: "Matrix", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "5", imdbId: "imdb05", title: "Canta", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "6", imdbId: "imdb06", title: "Scream", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "7", imdbId: "imdb07", title: "Spiderman No way home", overView: "Lorem ipsum", posterPath: nil),
]

ViewModel

El siguiente paso será crear nuestro ViewModel, es decir, la parte que se encargará de leer datos y enlazarlos a la vista.

Crearemos un archivo llamado HomeViewModel con lo siguiente:

import SwiftUI

class HomeViewModel: ObservableObject {
    
    @Published var movies: [Movie] = []
    
    func loadMovies() {
        self.movies = moviesTest
    }
    
}

Tenemos 3 puntos a destacar. Por un lado, nuestra clase ViewModel implementa ObservableObject, lo cual permitirá que se observe desde la vista.

Por otro lado, tenemos una propiedad marcada con @Published, el cual es un wrapper que nos permitirá enviar datos y que será observado desde la vista. La vista reaccionará a los cambios que se vayan produciendo desde aquí.

Por último, tenemos un método loadMovies que se encarga de lanzar la lectura de datos. En este caso concreto, simplemente estamos enlazando con nuestros datos mock, pero en futuras publicaciones se complicará esta parte, mientras que la vista permanecerá igual.

Ahora que tenemos 2 de 3 partes, vamos a por la última.

View

Esta es sin duda la más sencilla. De momento tendremos un listado que se nutrirá de los datos que vienen del viewModel.

struct HomeView: View {
    
    @ObservedObject var viewModel: HomeViewModel = HomeViewModel()
    
    var body: some View {
        VStack {
            ForEach(viewModel.movies) { movie in
                Text(movie.title)
            }
        }.onAppear {
            viewModel.loadMovies()
        }
    }
}

Para empezar definimos nuestro viewModel y lo marcamos con @ObservedObject, esto es un wrapper que le dirá a la vista que se va a observar este objeto.

Después, tenemos un VStack, en el cual añadimos un ForEach del viewModel.movies, junto con un Text, donde de momento solo mostraremos el título de nuestra película.

Hasta aquí, de por si, no pasará nada, pero gracias al modificador onAppear llamaremos al método que lee las películas en el momento en el que aparece la vista.

Y con esto ya hemos montado una arquitectura MVVM muy básica. Aunque muy básica ya hemos marcado los cimientos y separado los conceptos para construir nuestra aplicación.

Como siempre, os dejo enlace al repositorio de Github donde tengo el código completo. No olvidéis revisar el vídeo de Youtube asociado, suscribiros y dejar un gran like.

https://github.com/3pies/moviesios

Descubre como crear tu app iOS de cero a experto

Hace poco os presentaba una app Android que había creado para un proceso de selección. Podéis ver aquí el código de la aplicación https://github.com/3pies/movies

Además del video presentación que hice en mi canal de Youtube.

Y como no podía ser de otra forma, no podemos dejar a nuestros queridos usuarios de iOS sin su versión. Así que empezaré a crear en iOS una versión muy similar. En este caso utilizando SwiftUI.

Hoy nos tocará preparar nuestra app. Para ello lo primero que haremos será crear un nuevo proyecto, indicamos donde lo almacenaremos, en mi caso marcaré crear repositorio git (os recomiendo que vosotros también lo hagáis).

Como interface SwiftUI y lenguaje Swift. En mi caso no utilizaré Core Data, así que lo dejo sin marcar. Indicamos el Bundle Identifier y ya tendremos el proyecto listo.

Si recordáis, en el proyecto Android, tenía que:

  • Leer datos de un WS
  • Almacenar en base de datos local

Así que aprovecharemos y añadiremos nuestras primeras librerías. Para ello vamos a utilizar Swift Package Manager. Este es un gestor de dependencias similar a Cocoapods, y que ya viene integrado en el IDE XCode.

Para añadir nuevas librerías es muy sencillo, primero localizamos las que queremos añadir. En mi caso, voy a añadir primero Alamofire. Librería utilizada para hacer las peticiones a los WS. Vamos a su github y allí nos aparecen las instrucciones para añadir https://github.com/Alamofire/Alamofire#installation

En XCode, vamos a File -> Add Packages… En la ventana que aparece, arriba a la izquierda, en el buscador escribimos «https://github.com/Alamofire/Alamofire.git» y pulsamos «Add Package». Empezará a cargar hasta que finalmente acabe.

Una vez lista esta podemos añadir alguna más. Os dejo enlaces a los repositorios de:

  • Realm para almacenar de forma local https://github.com/realm/realm-swift.git
  • Resolver para añadir inyección de dependencias https://github.com/hmlongco/Resolver.git

Llegados a este punto, podremos ir a las propiedades de nuestro proyecto, en la sección «Package Dependencies» tendremos todas las dependencias que hemos añadido.

Si en cualquier momento estamos interesados en actualizar nuestras dependencias podemos ir a File -> Packages -> Update to Latest Package versions.

Una vez listas nuestras primeras librerías, ya estaremos preparados para empezar a desarrollar nuestra aplicación.

Por último, os dejo el enlace al repositorio donde voy a hacer la aplicación para iOS https://github.com/3pies/moviesios

Mis Mocks no funcionaban, así es como lo arreglé

Hace poco os compartí un proyecto Android en Github. Se trata de una aplicación para leer películas desde un web service. Aquí os dejo de nuevo el enlace https://github.com/3pies/movies

A partir de ahora empezaré a testearla poco a poco. Hoy empezaremos a preparar nuestra app para testing.

Para testear nuestras clases, lo habitual es crear clases mock que nos ayuden a centrarnos en la clase que estamos testando. Estos mocks se comportarán tal como nosotros queremos para que el resultado del test sea siempre predecible.

Vamos al lío. Lo primero que debemos hacer es añadir las librerías necesarias al proyecto. Para ello, abrimos el archivo build.gradle de nuestra aplicación y añadimos las dependencias necesarias.

Por un lado debemos añadir junit a nuestro proyecto, para tener las funcionalidades básicas de test.

testImplementation 'junit:junit:4.13.2'

Y además la librería Mockito. Fijaros que lo hacemos con «testImplementation», de este modo, las librerías solo se cargarán cuando ejecutemos los test unitarios.

testImplementation "org.mockito:mockito-core:4.6.1"
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"

Sincronizamos y podemos empezar nuestros tests. Pero, no os vayáis todavía, que queda una sorpresa.

Voy a crear un ejemplo de clase para probar. En el repositorio lo podréis encontrar en el archivo Playground.kt, el cual usaré para diferentes pruebas.

class Bar() {
    fun sayHello(): String { return "hello" }
}

class Foo constructor(private var bar: Bar) {
    fun saySomething(): String {
        return bar.sayHello()
    }
}

Ok, tenemos una clase Foo que usa Bar para devolver un texto. Desde Foo el método saySomething llama al método sayHello de bar. Creemos un test para verificar que esto siempre se cumple.

Vamos a crear nuestro primer test unitario sobre la clase Foo. Para ello, con la vista Android seleccionada, vamos a la carpeta de test. Es la que tiene el nombre de nuestro namespace y entre paréntesis aparece test.

Creamos otro archivo para pruebas, le llamaré PlaygroundTest.kt

Creamos nuestra clase de Test y definimos un mock para Bar. Nuestra clase la marcamos con una anotation que indicará que se ejecuta con Junit, y para nuestro mock hacemos uso de Mockito.mock. Simple.

@RunWith(JUnit4::class)
class FooTest {

    private val bar = Mockito.mock(Bar::class.java)
    
}

Y dentro de esta clase debemos crear nuestro primer test. Veámoslo.

@Test
fun `Test say something`() {
    Mockito.`when`(bar.sayHello()).thenReturn("goodbye")

    val foo = Foo(bar)

    val result = foo.saySomething()

    verify(bar, times(1)).sayHello()
    assertThat(result, CoreMatchers.`is`("goodbye"))
}

Lo primero que hacemos es definir como se comportará el mock de Bar cuando llamemos a «sayHello». Seguimos instanciando un objeto Foo y llamando el método a testear: saySomething. Y por último, verificamos que se ejecuta sayHello y el resultado es el que se espera. Fácil, no? Pues no.

Ejecutad este test y veréis que da error. Aquí es donde quería llegar. El problema es que nuestra clase Bar y el método sayHello necesitan que se declaren como open.

Podéis probar a marcar como open Bar y su método y volver a ejecutar, veréis que ahora se ejecuta correctamente.

open class Bar() {
    open fun sayHello(): String { return "hello" }
}

Ya está, se acabó, no? Pues no.

Si ya es público, ¿porque necesito abrirlo aún más? Deberíamos dar a las clases el nivel de privacidad más restrictivo posible e ir abriendo solo según se necesite. En este caso, marcar como open clase y método no es la solución ideal, ya que a partir de ahora todas nuestras clases y métodos debemos marcarlos así para poder testear. 🙁

Si al menos tuviéramos algo que nos marcase nuestra clase como open solo cuando ejecutamos los tests… Ehhh, que si que existe eso.

Simplemente haced esto, dentro de la carpeta app de nuestro proyecto creamos carpeta resources, y dentro de esta otra carpeta que llamamos mockito-extensions. En esta carpeta creamos un archivo y le llamamos org.mockito.plugins.MockMaker. En este archivo añadimos la siguiente línea:

mock-maker-inline

Ahora eliminamos la instrucción open de nuestra clase Bar, y listo. Probar a ejecutar el test de nuevo. Ahora funciona correctamente, y nuestras clases y métodos podrán tener la visibilidad que le corresponda.

Creedme, esto puede llegar a ser un quebradero de cabeza para quien se inicia a testear una app. Probadlo y me contáis que tal os fue.

iOS – Alamofire en 3 sencillos pasos

Cuando hacemos aplicaciones móviles, existen ciertas cosas que solemos necesitar en la mayoría de los casos. Una de ellas es nutrir nuestra aplicación con datos, en este caso veremos cómo leer datos de nuestra API HTTP.

Para poder leer datos de nuestras APIs lo más común es utilizar una librería que nos de una serie de utilidades ya empaquetadas y listas para utilizar. No queremos reinventar la rueda una y otra vez. Por eso, en este ejercicio, utilizatemos Alamofire, una de las librerías más populares.

Para empezar, necesitamos añadirlo a nuestro Podfile. Y en nuestra terminal ejecutamos un pod install para que se descargue.

También veremos como con el uso de objetos Codable, mapear las respuestas de nuestra API es muy sencillo.

Si no te quieres perder la explicación, haz click en el video. Y no olvides dejar tus comentarios o preguntas. Y si te gusta el vídeo, y quieres más, no olvides darle like y suscribirte.