Saltar al contenido principal

Pruebas de interfaz de usuario en Kotlin | Cómo implementarlas

Si miramos la serie de laboratorios de cógigo “Advanced Android in Kotlin” en Google podemos encontrar uno sobre Testing y Dependency Injection. No solo es una práctica de testing sino una guía de arquitectura para Android también.

Cuando llegues a la tercera sección empezarás a escribir test de interfaz de usuario con Espresso. Es impresionante ejecutarlos y ver la aplicación funcionando sola y haciendo cosas. Aunque está archivado como un laboratorio de código avanzado, el código para el test puede ser refactorizado usando el Page Object Pattern.

Page Object Pattern

El objetivo de un Page Object es ocultar el código «extraño» necesario para replicar los pasos necesarios para navegar por nuestra aplicación, realizar acciones como usuario y proporcionar una interfaz limpia con la que crear pruebas útiles y con sentido.

Para ver esto en acción usaremos el código del repo de código del laboratorio solución final que puedes encontrar aquí.

Trabajaremos desde la rama «end_codelab_3».

Page Object en Kotlin

Comenzaremos añadiendo la clase base Kotlin Page que iniciará la magia. Añade esta clase al conjunto de fuentes «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 quieres profundizar en esto, echa un vistazo a Page Object Pattern in Kotlin for UI Test Automation On Android de Kate Savelova.

Al final, esta clase sirve para poder definir una API fluida que nos ayudará a organizar el código necesario para realizar la navegación, las acciones y las comprobaciones a lo largo de las párginas de nuestra app, las vistas, etc. Presta especial atención a la función verify. Esta función se utiliza para comprobar si la página que queremos se ha cargado.

Empecemos por añadir nuestra primera prueba con esta página. Ve al archivo AppNavigationTest.kt y añade un nuevo test que añada 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 de inicio para el test que acaba de lanzar el TasksActivity, para una comprensión profunda de esto echa un vistazo al laboratorio del código aquí.

El código completo del 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()

}

Observa la belleza de este test:

  • Crea una tarea
  • En TasksPage, pulsa el botón Add
  • En la página AddTaskPage, añade la tarea que hemos creado en el paso 1
  • Ahora en TasksPage, comprueba que la tarea ha sido añadida

Esto es muy significativo y simple. Aún no se compila, pero no te preocupes. Vamos a arreglarlo todo:

Añade la dependencia de gradle a la aplicación build.gradle:

  • implementation «org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion»

Clases TasksPage y AddTaskPage

Añade las clases TasksPage y AddTaskPage en el mismo paquete de 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

}

}

Contiene todo el código Espresso necesario para realizar todas las interacciones. Si no usamos este patrón, terminaremos con una prueba muy larga con todos esos métodos de Espresso en una sola prueba, lo que la hace muy díficil de leer y mantener.

Terminamos con esta aplicación funcionando en el móvil:

 

La prueba equivalente sin utilizar el patrón Page Object es:

@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é código de prueba prefieres?

Las ventajas de utilizar este patrón para construir pruebas de interfaz de usuario son:

  • Pruebas de UI significativas:
    • Como puedes ver, el código de test de UI es muy descriptivo, está escrito como una novela.
  • Mantenimiento:
    • Cada vez que la UI cambia, sólo debemos cambiar las páginas que se han visto afectadas por ello. Pero con esta arquitectura, es fácil encontrarlas y es más fácil averiguar por qué.
    • Añadir nuevas pruebas se convierte en algo fácil para evitar el código duplicado.

Conclusiones

Page Object es un gran patrón ampliamente utilizado para realizar pruebas de UI en otras plataformas como Java, JavaScript, C#, etc. Y como puedes ver es muy fácil una vez lo entiendes. Esperamos que este post te haya resultado útil!

Puedes ver una versión completa de este código en mi fork de google code.

Y si quieres convertirte en un experto de Ktolin, no te pierdas esta sesión con mi compañero Miguel Ángel Barrera, donde hablaremos sobre todos sus secretos.

Autor
Juan María Laó
Software Development Engineer