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í.