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.

à 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:
- pendant les tests, nous modifions les données;
- 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.