Kotlin DSL, accesorios y elegantes pruebas de IU en Android

Cuando conocí a Kotlin DSL, pensé: gran cosa, es una pena en el desarrollo de productos, no será útil. Sin embargo, me equivoqué: nos ayudó a crear una forma muy concisa y elegante de escribir pruebas de interfaz de usuario de extremo a extremo en Android.


imagen


Sobre el servicio, datos de prueba y por qué no es tan simple


Para empezar, un pequeño contexto sobre nuestro servicio, para que comprenda por qué tomamos ciertas decisiones.


Ayudamos a los buscadores de empleo y empleadores a encontrarse:


  • los empleadores registran sus empresas y publican vacantes
  • los solicitantes de empleo buscan trabajos, los agregan a favoritos, se suscriben a los resultados de búsqueda, crean currículums y envían comentarios

Para simular escenarios de usuarios reales y asegurarnos de que la aplicación funciona correctamente en ellos, necesitamos crear todos estos datos de prueba en el servidor. Usted dirá: "Así que cree empleadores de prueba y solicitantes de empleo por adelantado, y luego trabaje con ellos en las pruebas". Pero hay un par de problemas:


  1. durante las pruebas cambiamos los datos;
  2. Las pruebas se ejecutan en paralelo.

Entorno de prueba y accesorios


Las pruebas de extremo a extremo se ejecutan en bancos de prueba. Tienen casi un ambiente militar, pero no hay datos reales. En este sentido, al agregar nuevos datos, la indexación ocurre casi al instante.


Para agregar datos al stand, utilizamos métodos de fijación especiales. Agregan datos directamente a la base de datos y los indexan instantáneamente:


interface TestFixtureUserApi { @POST("fx/employer/create") fun createEmployerUser(@Body employer: TestEmployer): Call<TestEmployer> } 

Los accesorios están disponibles solo desde la red local y solo para bancos de pruebas. Los métodos se llaman desde la prueba inmediatamente antes de comenzar la Actividad de inicio.


DSL


Entonces llegamos a lo más jugoso. ¿Cómo se configuran los datos para la prueba?


 initialisation{ applicant { resume { title = "Resume for similar Vacancy" isOptional = true resumeStatus = ResumeStatus.APPROVED } resume { title = "Some other Resume" } } employer { vacancy { title = "Resume for similar Vacancy" } vacancy { title = "Resume for similar Vacancy" description = "Working hard" } vacancy { title = "Resume for similar Vacancy" description = "Working very hard" } } } 

En el bloque de inicialización, comenzamos las entidades necesarias para la prueba: en el ejemplo anterior, creamos un solicitante con dos hojas de vida, así como un empleador que proporcionó varias vacantes.


Para eliminar los errores asociados con la intersección de los datos de prueba, generamos un identificador único para la prueba y para cada entidad.


Relaciones entre entidades


¿Cuál es la principal limitación al trabajar con DSL? Debido a su estructura de árbol, es bastante difícil construir conexiones entre diferentes ramas de un árbol.


Por ejemplo, en nuestra solicitud para solicitantes hay una sección "Vacantes adecuadas para currículums". Para que las vacantes aparezcan en esta lista, necesitamos establecerlas de tal manera que estén relacionadas con el currículum del usuario actual.


 initialisation { applicant { resume { title = "TEST_VACANCY_$uniqueTestId" } } employer { vacancy { title = "TEST_VACANCY_$uniqueTestId" } } } 

Se utiliza un identificador de prueba único para esto. Por lo tanto, cuando se trabaja con la aplicación, se recomiendan las vacantes especificadas para este currículum. Además, es importante tener en cuenta que no aparecerán otras vacantes en esta lista.


Inicializar datos del mismo tipo.


Pero, ¿y si necesita hacer muchas vacantes? ¿Es cada bloque así que copia? Por supuesto que no! Hacemos un método con un bloque de vacantes, que indica el número requerido de vacantes y un transformador para diversificarlos dependiendo del identificador único.


 initialisation { employer { vacancyBlock { size = 10 transformer = { it.also { vacancyDsl -> vacancyDsl.description = "Some description with text ${vacancyDsl.uniqueVacancyId}" } } } } } 

En el bloque vacancyBlock, indicamos cuántos clones de vacantes necesitamos crear y cómo transformarlos dependiendo del número de serie.


Trabaja con datos en la prueba


Durante la prueba, trabajar con datos se vuelve muy simple. Todos los datos creados por nosotros están disponibles para nosotros. En nuestra implementación, se almacenan en contenedores especiales para colecciones. Los datos se pueden obtener de ellos tanto por el número de orden del trabajo (vacantes [0]), como por la etiqueta que se puede establecer en dsl (vacantes ["mi vacante"]), y por accesos directos (vacancies.first ()


TaggedItemContainer
 class TaggedItemContainer<T>( private val items: MutableList<TaggedItem<T>> ) { operator fun get(index: Int): T { return items[index].data } operator fun get(tag: String): T { return items.first { it.tag == tag }.data } operator fun plusAssign(item: TaggedItem<T>) { items += item } fun forEach(action: (T) -> Unit) { for (item in items) action.invoke(item.data) } fun first(): T { return items[0].data } fun second(): T { return items[1].data } fun third(): T { return items[2].data } fun last(): T { return items[items.size - 1].data } } 

En casi el 100% de los casos, al escribir pruebas, utilizamos los métodos first () y second (), el resto se mantiene para mayor flexibilidad. A continuación se muestra un ejemplo de una prueba con inicialización y pasos en Kakao


 initialisation { applicant { resume { title = "TEST_VACANCY_$uniqueTestId" } } }.run { mainScreen { positionField { click() } jobPositionScreen { positionEntry(vacancies.first().title) } searchButton { click() } } } 

Lo que no cabe en DSL


¿Pueden todos los datos encajar en DSL? Nuestro objetivo era mantener DSL lo más conciso y simple posible. En nuestra implementación, debido a que el orden de trabajo de los solicitantes y los empleadores no es importante, no es posible ajustar su relación: las respuestas.
La creación de respuestas ya se realiza en el siguiente bloque mediante operaciones en entidades ya creadas en el servidor.


Implementación de DSL


Como entendió en el artículo, el algoritmo para especificar datos de prueba y realizar la prueba es el siguiente:


  • Parte del DSL se analiza en la inicialización;
  • En función de los valores obtenidos, los datos de prueba se crean en el servidor;
  • Se ejecuta el bloque de transformación opcional, en el que puede establecer respuestas;
  • Se realiza una prueba con un conjunto de datos ya final.

Analizando datos de un bloque de inicialización


¿Qué tipo de magia está sucediendo allí? Considere cómo se construye el elemento TestCaseDsl de nivel superior:


 @TestCaseDslMarker class TestCaseDsl { val applicants = mutableListOf<ApplicantDsl>() val employers = mutableListOf<EmployerDsl>() val uniqueTestId = CommonUtils.unique fun applicant(block: ApplicantDsl.() -> Unit = {}) { val applicantDsl = ApplicantDsl( uniqueTestId, uniqueApplicantId = CommonUtils.unique applicantDsl.block() applicants += applicantDsl } fun employer(block: EmployerDsl.() -> Unit = {}) { val employerDsl = EmployerDsl( uniqueTestId = uniqueTestId, uniqueEmployerId = CommonUtils.unique employerDsl.block() employers += employerDsl } } 

En el método del solicitante, creamos ApplicantDsl.


SolicitanteDsl
 @TestCaseDslMarker class ApplicantDsl( val uniqueTestId: String, val uniqueApplicantId: String, var tag: String? = null, var login: String? = null, var password: String? = null, var firstName: String? = null, var middleName: String? = null, var lastName: String? = null, var email: String? = null, var siteId: Int? = null, var areaId: Int? = null, var resumeViewLimit: Int? = null, var isMailingSubscription: Boolean? = null ) { val resumes = mutableListOf<ResumeDsl>() fun resume(block: ResumeDsl.() -> Unit = {}) { val resumeDslBuilder = ResumeDsl( uniqueTestId = uniqueTestId, uniqueApplicantId = uniqueApplicantId, uniqueResumeId = CommonUtils.unique ) resumeDslBuilder.apply(block) this.resumes += resumeDslBuilder } } 

Luego realizamos operaciones desde el bloque de bloques: SolicitanteDsl. () -> Unidad. Es este diseño el que nos permite operar fácilmente con los campos ApplicantDsl en nuestro DSL.


Tenga en cuenta que uniqueTestId y uniqueApplicantId (identificadores únicos para conectar entidades entre ellos) en el momento de la ejecución del bloque ya están configurados y podemos acceder a ellos.


El bloque de inicialización internamente tiene una estructura similar:


 fun initialisation(block: TestCaseDsl.() -> Unit): Initialisation { val testCaseDsl = TestCaseDsl().apply(block) val testCase = TestCaseCreator.create(testCaseDsl) return Initialisation(testCase) } 

Creamos una prueba, le aplicamos acciones de bloque, luego usamos TestCaseCreator para crear datos en el servidor y ponerlos en colecciones. La función TestCaseCreator.create () es bastante simple: iteramos sobre los datos y los creamos en el servidor.


Errores e ideas


Algunas pruebas son muy similares y difieren solo en los datos entrantes y las formas de controlar sus pantallas (por ejemplo, cuando se indican diferentes monedas en la vacante).

En nuestro caso, hubo pocas pruebas de este tipo, y decidimos no saturar DSL con sintaxis especial


En los días previos a DSL, indexamos datos durante mucho tiempo y, para ahorrar tiempo, hicimos muchas pruebas en una clase y creamos todos los datos en un bloque estático.

No haga esto: le será imposible reiniciar la prueba caída. El hecho es que durante el lanzamiento de la prueba fallida, podríamos cambiar los datos iniciales en el servidor. Por ejemplo, podríamos agregar una vacante a sus favoritos. Luego, cuando reinicie la prueba, al hacer clic en el asterisco, por el contrario, se eliminará la vacante de la lista de favoritos, y este es un comportamiento que no esperamos.


Resumen


Este método de especificar datos de prueba simplificó enormemente el trabajo con pruebas:
Al escribir pruebas, no necesita pensar si hay un servidor y en qué orden necesita inicializar los datos;
Todas las entidades que se pueden configurar en el servidor aparecen fácilmente en sugerencias IDE;
Hay una única forma de inicializar y comunicar datos entre sí.


Materiales relacionados


Si está interesado en nuestro enfoque para las pruebas de IU, antes de comenzar, le sugiero que lea los siguientes materiales:



Que sigue


Este artículo es el primero de una serie sobre herramientas y marcos de alto nivel para escribir y admitir pruebas de IU en Android. A medida que haya nuevas piezas disponibles, las vincularé a este artículo.

Source: https://habr.com/ru/post/455042/


All Articles