Kotlin DSL, luminaires et tests d'interface utilisateur élégants dans Android

Quand j’ai rencontrĂ© Kotlin DSL, j’ai pensĂ©: super, c’est dommage dans le dĂ©veloppement de produits ça ne sera pas utile. Cependant, je me trompais: il nous a aidĂ©s Ă  crĂ©er un moyen trĂšs concis et Ă©lĂ©gant d'Ă©crire des tests d'interface utilisateur de bout en bout dans Android.


image


À propos du service, des donnĂ©es de test et pourquoi ce n'est pas si simple


Pour commencer, un peu de contexte sur notre service, afin que vous compreniez pourquoi nous avons pris certaines décisions.


Nous aidons les demandeurs d'emploi et les employeurs Ă  se retrouver:


  • les employeurs enregistrent leurs entreprises et affichent les postes vacants
  • les demandeurs d'emploi recherchent des emplois, les ajoutent aux favoris, s'abonnent aux rĂ©sultats de la recherche, crĂ©ent des CV et envoient des commentaires

Afin de simuler des scénarios d'utilisateurs réels et de s'assurer que l'application fonctionne correctement sur eux, nous devons créer toutes ces données de test sur le serveur. Vous direz: "Créez donc à l'avance des employeurs et des demandeurs d'emploi pour les tests, puis travaillez avec eux dans les tests." Mais il y a quelques problÚmes:


  1. pendant les tests, nous modifions les données;
  2. les tests se déroulent en parallÚle.

Environnement de test et équipements


Des tests de bout en bout s'exĂ©cutent sur des bancs de test. Ils ont presque un environnement militaire, mais il n'y a pas de donnĂ©es rĂ©elles. À cet Ă©gard, lors de l'ajout de nouvelles donnĂ©es, l'indexation se produit presque instantanĂ©ment.


Pour ajouter des données au stand, nous utilisons des méthodes de fixation spéciales. Ils ajoutent des données directement à la base de données et les indexent instantanément:


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

Les luminaires ne sont disponibles que sur le réseau local et uniquement pour les bancs d'essai. Les méthodes sont appelées à partir du test immédiatement avant de commencer l'activité de démarrage.


DSL


Nous sommes donc arrivés au plus juteux. Comment les données sont-elles définies pour le test?


 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" } } } 

Dans le bloc d'initialisation, nous démarrons les entités nécessaires au test: dans l'exemple ci-dessus, nous avons créé un demandeur d'emploi avec deux CV, ainsi qu'un employeur qui a fourni plusieurs postes vacants.


Pour éliminer les erreurs associées à l'intersection des données de test, nous générons un identifiant unique pour le test et pour chaque entité.


Relations entre entités


Quelle est la principale limitation lorsque vous travaillez avec DSL? En raison de sa structure arborescente, il est assez difficile d'établir des connexions entre les différentes branches d'un arbre.


Par exemple, dans notre candidature pour les candidats, il y a une section «Postes vacants appropriés pour les CV». Pour que les postes vacants apparaissent dans cette liste, nous devons les définir de maniÚre à ce qu'ils soient liés au curriculum vitae de l'utilisateur actuel.


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

Un identifiant de test unique est utilisé pour cela. Ainsi, lorsque vous travaillez avec l'application, les postes vacants spécifiés sont recommandés pour ce CV. De plus, il est important de noter qu'aucun autre poste vacant n'apparaßtra sur cette liste.


Initialiser des donnĂ©es du mĂȘme type


Mais que se passe-t-il si vous devez faire beaucoup de postes vacants? Est-ce que chaque bloc est copié? Bien sûr que non! Nous faisons une méthode avec un bloc de postes vacants, qui indique le nombre de postes vacants requis et un transformateur pour les diversifier en fonction de l'identifiant unique.


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

Dans le bloc vacancyBlock, nous indiquons combien de clones de postes vacants nous devons créer et comment les transformer en fonction du numéro de série.


Travailler avec des données dans le test


Pendant le test, travailler avec des donnĂ©es devient trĂšs simple. Toutes les donnĂ©es que nous crĂ©ons sont Ă  notre disposition. Dans notre implĂ©mentation, ils sont stockĂ©s dans des emballages spĂ©ciaux pour les collections. Les donnĂ©es peuvent ĂȘtre obtenues auprĂšs d'eux Ă  la fois par le numĂ©ro de sĂ©rie du travail (postes vacants [0]), et par la balise qui peut ĂȘtre dĂ©finie dans dsl (postes vacants [«mon poste vacant»]), et par des raccourcis (postes vacants.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 } } 

Dans presque 100% des cas, lors de l'écriture des tests, nous utilisons les premiÚre () et seconde () méthodes, les autres sont conservées pour plus de flexibilité. Voici un exemple de test avec initialisation et étapes sur Kakao


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

Ce qui ne rentre pas dans DSL


Toutes les donnĂ©es peuvent-elles entrer dans DSL? Notre objectif Ă©tait de garder DSL aussi concis et simple que possible. Dans notre mise en Ɠuvre, Ă©tant donnĂ© que l'ordre de travail des candidats et des employeurs n'est pas important, il n'est pas possible d'adapter leur relation - les rĂ©ponses.
La création de réponses est déjà effectuée dans le bloc suivant par des opérations sur des entités déjà créées sur le serveur.


Implémentation DSL


Comme vous l'avez compris dans l'article, l'algorithme pour spécifier les données de test et effectuer le test est le suivant:


  • Une partie de la DSL est analysĂ©e lors de l'initialisation;
  • Sur la base des valeurs obtenues, des donnĂ©es de test sont créées sur le serveur;
  • Le bloc de transformation facultatif est exĂ©cutĂ©, dans lequel vous pouvez dĂ©finir des rĂ©ponses;
  • Un test est effectuĂ© avec un jeu de donnĂ©es dĂ©jĂ  final.

Analyser les données d'un bloc d'initialisation


Quel genre de magie se passe là-bas? Considérez comment l'élément de niveau supérieur TestCaseDsl est construit:


 @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 } } 

Dans la méthode du demandeur, nous créons ApplicantDsl.


ApplicantDsl
 @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 } } 

Ensuite, nous effectuons des opérations sur celui-ci à partir du bloc bloc: ApplicantDsl. () -> Unit. C'est cette conception qui nous permet de fonctionner facilement avec les champs ApplicantDsl dans notre DSL.


Veuillez noter que uniqueTestId et uniqueApplicantId (identifiants uniques pour connecter des entités entre elles) au moment de l'exécution du bloc sont déjà définis et nous pouvons y accéder.


Le bloc d'initialisation a en interne une structure similaire:


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

Nous créons un test, lui appliquons des actions de blocage, puis utilisons TestCaseCreator pour créer des données sur le serveur et les placer dans des collections. La fonction TestCaseCreator.create () est assez simple - nous parcourons les données et les créons sur le serveur.


PiÚges et idées


Certains tests sont trÚs similaires et ne diffÚrent que par les données d'entrée et les moyens de contrÎler leurs affichages (par exemple, lorsque différentes devises sont indiquées dans le poste vacant).

Dans notre cas, il y avait peu de ces tests, et nous avons décidé de ne pas encombrer DSL avec une syntaxe spéciale


Avant la DSL, nous avons longtemps indexé les données et pour gagner du temps, nous avons fait beaucoup de tests dans une classe et créé toutes les données dans un bloc statique.

Ne faites pas cela - il vous sera impossible de redémarrer le test tombé. Le fait est que lors du lancement du test tombé, nous pourrions changer les données initiales sur le serveur. Par exemple, nous pourrions ajouter un poste vacant à vos favoris. Ensuite, lorsque vous relancez le test, un clic sur l'astérisque entraßnera au contraire la suppression de la vacance de la liste des favoris, et c'est un comportement auquel on ne s'attend pas.


Résumé


Cette méthode de spécification des données de test a grandement simplifié le travail avec les tests:
Lorsque vous écrivez des tests, vous n'avez pas besoin de vous demander s'il existe un serveur et dans quel ordre vous devez initialiser les données;
Toutes les entitĂ©s qui peuvent ĂȘtre dĂ©finies sur le serveur apparaissent facilement dans les conseils IDE;
Il existe un moyen unique d'initialiser et de communiquer des données entre eux.


Matériel connexe


Si vous ĂȘtes intĂ©ressĂ© par notre approche des tests d'interface utilisateur, alors avant de commencer, je vous suggĂšre de vous familiariser avec les documents suivants:



Et ensuite


Cet article est le premier d'une série sur les outils et les cadres de haut niveau pour écrire et prendre en charge les tests d'interface utilisateur dans Android. Lorsque de nouvelles piÚces seront disponibles, je les lierai à cet article.

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


All Articles