UI tests en Kotlin

Si os pasáis por la serie de codelabs de google Advanced Android in Kotlin veréis uno sobre Testing and Dependency Injection. No es sólo un ejercicio de test sino que es toda una guia de arquitectura para aplicaciones Android.

Cuando llegamos a la tercera sección empezamos a escribir tests de interfaces de usuario con Espresso. La primera vez que lo ejecutamos impresiona ver cómo nuestra app comienza a ejecutarse y a hacer cosas sola. Aunque está archivado como un codelab avanzado, aún se puede exprimir un poco más introduciendo el patrón Page Object.

Patrón Page Object

El sentido del page object es esconder todo el código necesario para replicar los pasos que hay que hacer para navegar por la app, realizar acciones como un usuario etc… y ofrecer una interfaz limpia que nos permita escribir tests más semánticos y mantenibles.

Para verlo en acción, hemos usado el propio código de la solución final del codelab que podéis encontrar aquí:

https://github.com/googlecodelabs/android-testing/tree/end_codelab_3

Trabajaremos a partir de la rama “end_code_lab3”.

Page Object

Comenzamos añadiendo esta clase Page que hace gran parte de la magia. La añadimos al source set “androidTest”:

open class Page {
    companion object {
        inline fun <reified T : Page> on(): T {
            return Page().on()
        }
    }

    inline fun <reified T : Page> on(): T {
        val page = T::class.constructors.first().call()
        page.verify()
        //Thread.sleep(500) //To give time when asynchronous calls are invoked
        return page
    }

    open fun verify(): Page {
        // Each subpage should have its default assurances here
        return this
    }

    fun back(): Page {
        Espresso.pressBack()
        return this
    }
}

Si queréis profundizar más en esto, pasaros por Page Object Pattern in Kotlin for UI Test Automation On Android de Kate Savelova.

Al final, esta clase se usa para poder definir una API fluida que nos permitirá organizar el código que necesitamos para navegar, realizar acciones, comprobar cosas en las páginas, vistas, etc. Prestad especial atención a la función verify. Esta función se usa para comprobar si la página que queremos se ha cargado.

Vamos a añadir nuestro primer test. Vamos al archivo AppNavigationTest.kt y añadamos un nuestro test que añadirá una nueva tarea a la app:

@Test
fun createNewTask()  {
    // Start up Tasks screen
    val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
    dataBindingIdlingResource.monitorActivity(activityScenario)
   
    // When using ActivityScenario.launch, always call close()
    activityScenario.close()
}

Este es el código inicial para el test que simplemente lanza la TaskActivity. Para entenderlo mejor, leed el code lab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#0. Quiero centrarme en explicar cómo aplicar el patrón Page Object y no explicar todo lo que enseña el code lab ^_^

El código completo de nuestro test es:

@Test
fun createNewTask()  {
    // Start up Tasks screen
    val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
    dataBindingIdlingResource.monitorActivity(activityScenario)

    val testTask= Task("title", "description")

    Page.on<TasksPage>()
        .tapOnAddButton()
        .on<AddTaskPage>()
        .addTask(testTask)
        .on<TasksPage>()
        .checkAddedTask(testTask)

    // When using ActivityScenario.launch, always call close()
    activityScenario.close()
}

La “belleza” del test es que nos dice lo que hace de manera muy semántica:

  1. Crea una task
  2. En la página TasksPage, hace tap en el boton Add
  3. En la página AddTaskPage, añade la task que creamos en el paso 1
  4. Ahora en la página TaskPage, comprueba que se ha añadido la task.

Es muy semántico, y simple. Pero aún no compila, no te preocupes. Vamos a arreglarlo:

Añadimos la dependencia de gradle en el build.gradle de la app:

  • implementation “org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion”

Clases TasksPage y AddTaskPage

Vamos a añadir las clases TasksPage y AddTaskPage en el mismo paquete donde está nuestro PageObject:

class TasksPage : Page() {
    override fun verify(): TasksPage {
        onView(withId(R.id.tasks_container_layout))
            .check(matches(isDisplayed()))
        return this
    }

    fun tapOnAddButton(): TasksPage {
        onView(withId(R.id.add_task_fab)).perform(ViewActions.click())
        return this
    }


    fun tapOnEditTask(): TasksPage {
        onView(withId(R.id.edit_task_fab)).perform(ViewActions.click())
        return this
    }

    fun checkAddedTask(testTask: Task): TasksPage {
        onView(withText(testTask.title))
        return this
    }
}

class AddTaskPage: Page() {
    override fun verify(): AddTaskPage {
        Espresso.onView(withId(R.id.add_task_title_edit_text))
            .check(ViewAssertions.matches(isDisplayed()))
        return this
    }

    fun addTask(task: Task):AddTaskPage{
        onView(withId(R.id.add_task_title_edit_text))
            .perform(clearText(), typeText(task.title))
        onView(withId(R.id.add_task_description_edit_text))
            .perform(clearText(), typeText(task.description))
        onView(withId(R.id.save_task_fab)).perform(click())
        return this
    }
}

En estas clases está todo el código de Espresso que necesitamos para hacer las interacciones, pero bien ordenadito y recogidito. Si no lo hiciéramos así, terminaríamos con un test muy largo, con todos esos métodos de Espersso en un solo test, con lo que tendríamos un test difícil de leer y mantener. Además, si hiciésemos más tests de este tipo, tendríamos bastante código repetido entre los tests.

Si ejecutamos el test, el resultado será similar a:

Running test

Veamos el test equivalente sin usar el patrón PageObject:

@Test
fun createNewTaskWithoutPageObject(){
    val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
    dataBindingIdlingResource.monitorActivity(activityScenario)

    val task= Task("title", "description")

    //check tasks page open
    onView(withId(R.id.tasks_container_layout))
        .check(matches(isDisplayed()))

    //tap on add button
    onView(withId(R.id.add_task_fab)).perform(ViewActions.click()) 

    //check task page is open
    onView(withId(R.id.add_task_title_edit_text))
        .check(ViewAssertions.matches(isDisplayed()))

    //add task
    onView(withId(R.id.add_task_title_edit_text))
        .perform(ViewActions.clearText(), ViewActions.typeText(task.title))
    onView(withId(R.id.add_task_description_edit_text))
        .perform(ViewActions.clearText(), ViewActions.typeText(task.description))
    onView(withId(R.id.save_task_fab)).perform(click())

    //check task page is open
    onView(withId(R.id.tasks_container_layout))
        .check(matches(isDisplayed()))

    //check added task
    onView(withText(task.title))

    // When using ActivityScenario.launch, always call close()
    activityScenario.close()
}

¿Qué test preferirías mantener?

Los beneficios de usar este patrón para crear test de interfaz de usuario son:

  • Test semánticos: Como podemos ver, el código es muy descriptivo, está escrito como si fuera una novela.
  • Mantenimiento:
    • Cada vez que cambie la interfaz, sólo cambiaremos aquellas páginas que se han visto afectados. Pero con esta arquitectura, es fácil encontrarlos, y es más fácil averiguar porqué ha fallado.
    • Añadir nuevos test es más rápido ya que no tendremos que estar duplicando código.

Resumen

El patrón Page Object está muy extendido a la hora de hacer tests de UI en otras plataformas como Java, JavaScript, C#. Una vez que se entiende, como me pasa a mí, algo hace clic en la cabeza. Y es muy fácil de aplicar.

Si quereis verlo en directo, podeis pasaros por el fork que dejo disponible en: https://github.com/juanlao/android-testing/tree/end_codelab_3

Espero que os sirva de algo estas líneas.

Referencias

https://www.elmosoft.net/pageobject-pattern-in-kotlin-for-ui-test-automation-on-android/

https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey

Create your website with WordPress.com
Get started