Scénario - Pas un objet de page

Au fil du temps, apporter des modifications à n'importe quel produit devient plus difficile, et il y a un risque croissant non seulement de publier de nouvelles fonctionnalités, mais aussi de casser les anciennes. Souvent, au lieu de vérifier manuellement l'ensemble du projet, ils essaient d'automatiser ce processus. Si vous parlez à des personnes qui testent des interfaces et parcourez des conférences, il devient clair que Selenium règne dans le monde des tests Web, et la grande majorité utilise Page Object comme organisation du code.


Mais pour moi, en tant que programmeur, pour une raison quelconque, je n'ai jamais aimé ce modèle et ce code que j'ai vus avec différentes équipes - les lettres SOLID sonnaient dans ma tête. Mais j'étais prêt à accepter le fait que les testeurs écrivent du code comme ils le souhaitent, en raison du manque d'alternatives, comme il y a environ un an, sur Angular Connect, j'ai entendu un rapport sur le test d'applications angulaires à l'aide du modèle de scénario. Maintenant, je veux partager.



Lapin expérimental


Tout d'abord, une brève description des outils utilisés. Comme exemple d'implémentation, je prendrai SerenityJS du rapport original, avec TypeScript comme langage des scripts de test.


Nous allons mettre des expériences sur l' application TODO - une application pour créer une simple liste de tâches. À titre d'exemples, nous utiliserons le code de Jan Molak , le créateur de la bibliothèque (code TypeScript, donc le point culminant est allé un peu).


Commençons en 2009


À cette époque, Selenium WebDriver est apparu et les gens ont commencé à l'utiliser. Malheureusement, dans un grand nombre de cas, en raison de la nouveauté de la technologie et du manque d'expérience en programmation, c'est faux. Les tests se sont avérés être du copier-coller complet, non pris en charge et "fragiles".


Le résultat est une rétroaction négative et une mauvaise réputation avec les autotests. Finalement, Simon Stewart, créateur de Selenium WebDriver, a répondu par contumace dans son article Mes tests de sélénium ne sont pas stables! . Le message général: "si vos tests sont fragiles et ne fonctionnent pas bien - ce n'est pas à cause du sélénium, mais parce que les tests eux-mêmes ne sont pas très écrits."


Pour comprendre ce qui est en jeu, examinons le scénario utilisateur suivant:


Feature: Add new items to the todo list In order to avoid having to remember things that need doing As a forgetful person I want to be able to record what I need to do in a place where I won't forget about them Scenario: Adding an item to a list with other items Given that James has a todo list containing Buy some cookies, Walk the dog When he adds Buy some cereal to his list Then his todo list should contain Buy some cookies, Walk the dog, Buy some cereal 

L'implémentation naïve "sur le front" ressemblera à ceci:


 import { browser, by, element, protractor } from 'protractor'; export = function todoUserSteps() { this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { element(by.id('new-todo')).sendKeys(item, protractor.Key.ENTER); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { element(by.id('new-todo')) .sendKeys(itemName, protractor.Key.ENTER) .then(callback); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(element.all(by.repeater('todo in todos')).getText()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); }; 

Comme vous pouvez le voir, une API de bas niveau est utilisée ici, des manipulations avec le DOM, un copier-coller des sélecteurs css. Il est clair qu'à l'avenir, même avec un petit changement dans l'interface utilisateur, vous devrez changer le code à de nombreux endroits.


Comme solution, Selenium devait proposer quelque chose d'assez bon pour se débarrasser de ces problèmes, et en même temps accessible aux personnes ayant peu ou pas d'expérience en programmation orientée objet. Une telle solution était Page Object, un modèle d'organisation du code.


Martin Fowler le décrit comme un objet d'abstraction autour d'une page HTML ou de son fragment, vous permettant d'interagir avec les éléments de la page sans toucher au HTML lui-même. Idéalement, un tel objet devrait permettre au code client de faire et de voir tout ce qu'une personne peut faire et voir.


Après avoir réécrit notre exemple conformément à ce modèle, nous avons ce qui suit:


 import { browser, by, element, protractor } from 'protractor'; class TodoList { What_Needs_To_Be_Done = element(by.id('new-todo')); Items = element.all(by.repeater('todo in todos')); addATodoItemCalled(itemName: string): PromiseLike<void> { return this.What_Needs_To_Be_Done.sendKeys(itemName, protractor.Key.ENTER); } displayedItems(): PromiseLike<string[]> { return this.Items.getText(); } } export = function todoUserSteps() { let todoList = new TodoList(); this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { todoList.addATodoItemCalled(item); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { todoList.addATodoItemCalled(itemName).then(() => callback()); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(todoList.displayedItems()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); }; 

À première vue, cela semble bon - nous nous sommes débarrassés de la duplication et de l'encapsulation supplémentaire. Mais il y a un problème: très souvent, ces classes dans le processus de développement atteignent des tailles énormes .



Vous pouvez essayer de décomposer ce code en composants indépendants, mais si le code atteint un tel état, il est déjà si étroitement couplé qu'il peut être très difficile et nécessitera de réécrire à la fois la classe elle-même et le code client entier (cas de test) qui utilisent ceci l'objet. En fait, une énorme classe n'est pas une maladie, mais un symptôme ( odeur de code ). Le principal problème est la violation du principe de responsabilité unique et du principe ouvert et fermé . Page Object implique une description de la page et de toutes les façons d'interagir avec elle dans une seule entité, qui ne peut pas être développée sans changer son code.


Pour nos applications de responsabilité de classe TODO sont les suivantes (photos de l' article ):
Responsabilité


Si vous essayez de séparer cette classe sur la base des principes de SOLID, vous obtenez quelque chose comme ceci:


Modèle de domaine


Le prochain problème que nous avons avec Page Object est qu'il fonctionne sur les pages. Dans tout test d'acceptation, il est courant d'utiliser des user stories. Le développement basé sur le comportement (BDD) et le langage Gherkin correspondent parfaitement à ce modèle. Il est entendu que le chemin de l'utilisateur vers l'objectif (scénario) est plus important que l'implémentation spécifique. Par exemple, si vous utilisez un widget de connexion dans tous les tests, lorsque vous modifiez la méthode de connexion (le formulaire a été déplacé, est passé à l'authentification unique), vous devrez modifier tous les tests. Dans les projets particulièrement dynamiques, cela peut s'avérer coûteux et long et pousser l'équipe à reporter les tests d'écriture jusqu'à stabilisation des pages (même si tout est clair avec les cas d'utilisation).


Pour résoudre ce problème, il vaut la peine de le regarder de l'autre côté. Nous introduisons les concepts suivants:


  1. RĂ´les - pour qui est-ce tout?
  2. Objectifs - pourquoi sont-ils (utilisateurs) ici et que veulent-ils atteindre?
  3. Tâches - Que doivent-ils faire pour atteindre ces objectifs?
  4. Actions - comment exactement un utilisateur doit-il interagir avec une page pour effectuer une tâche?

Ainsi, chaque script de test devient, de facto, un script pour l'utilisateur, destiné à l'exécution d'une user story spécifique. Si vous combinez cette approche avec les principes de programmation orientée objet décrits ci-dessus, le résultat sera un modèle de scénario (ou de parcours utilisateur ). Ses idées et principes ont été exprimés pour la première fois en 2007, avant même PageObject.


Scénario


Nous réécrivons notre code en utilisant les principes résultants.


James est l'acteur qui jouera notre utilisateur dans ce scénario. Il sait utiliser un navigateur.


 let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser)); 

L'objectif de James est d'ajouter le premier élément à sa liste:


  Scenario: Adding an item to a list with other items 

Pour atteindre cet objectif, James a besoin des éléments suivants:


  1. commencer par une liste contenant plusieurs éléments,
  2. ajouter un nouvel élément.

Nous pouvons diviser cela en deux classes:


 import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Open } from 'serenity-js/lib/screenplay-protractor'; import { AddATodoItem } from './add_a_todo_item'; export class Start implements Task { static withATodoListContaining(items: string[]) { return new Start(items); } performAs(actor: PerformsTasks) { return actor.attemptsTo( Open.browserOn('/examples/angularjs/'), ...this.addAll(this.items) ); } constructor(private items: string[]) { } private addAll(items: string[]): Task[] { return items.map(item => AddATodoItem.called(item)); } } 

L'interface de Task nécessite de définir la méthode performAs , où l'acteur sera transféré pendant l'exécution. attemptsTo - combinateur accepte n'importe quel nombre de shuffles. Ainsi, vous pouvez créer une variété de séquences. En fait, tout ce que cette tâche fait est d'ouvrir un navigateur sur la page souhaitée et d'y ajouter des éléments. Voyons maintenant la tâche d'ajouter un élément:


 import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Enter } from 'serenity-js/lib/screenplay-protractor'; import { protractor } from 'protractor'; import { TodoList } from '../components/todo_list'; export class AddATodoItem implements Task { static called(itemName: string) { return new AddATodoItem(itemName); } // required by the Task interface performAs(actor: PerformsTasks): PromiseLike<void> { // delegates the work to lower-level tasks return actor.attemptsTo( Enter.theValue(this.itemName) .into(TodoList.What_Needs_To_Be_Done) .thenHit(protractor.Key.ENTER) ); } constructor(private itemName: string) } } 

C'est plus intéressant ici, des actions de bas niveau apparaissent - entrez du texte dans l'élément de page et appuyez sur Entrée. TodoList est tout ce qui reste de la partie «descriptive» de l'objet Page, nous avons ici des sélecteurs CSS:


 import { Target, Question, Text } from 'serenity-js/lib/screenplay-protractor'; import { by } from 'protractor'; export class TodoList { static What_Needs_To_Be_Done = Target .the('"What needs to be done?" input box') .located(by.id('new-todo')); static Items = Target .the('List of Items') .located(by.repeater('todo in todos')); static Items_Displayed = Text.ofAll(TodoList.Items); } 

Ok, il reste à vérifier qu'après toutes les manipulations les informations correctes sont affichées. SerenityJS propose l'interface Question<T> - une entité qui renvoie une valeur affichée ou une liste de valeurs ( Text.ofAll dans l'exemple ci-dessus). Comment pourrait-on implémenter une question qui renvoie le texte d'un élément HTML:


 export class Text implements Question<string> { public static of(target: Target): Text { return new Text(target); } answeredBy(actor: UsesAbilities): PromiseLike<string[]> { return BrowseTheWeb.as(actor).locate(this.target).getText(); } constructor(private target: Target) { } } 

Ce qui est important, la liaison du navigateur est facultative. BrowseTheWeb est juste une capacité , qui vous permet d'interagir avec le navigateur. Vous pouvez implémenter, par exemple, la capacité RecieveEmails , qui permet à l'acteur de lire des lettres (pour l'inscription sur le site).


En mettant tout cela ensemble, nous obtenons ce schéma (de Jan Molak ):


et le scénario suivant:


 let actor: Actor; this.Given(/^.*that (.*) has a todo list containing (.*)$/, function (name: string, items: string) { actor = Actor.named(name).whoCan(BrowseTheWeb.using(protractor.browser)); return actor.attemptsTo( Start.withATodoListContaining(listOf(items)) ); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, function (itemName: string) { return actor.attemptsTo( AddATodoItem.called(itemName) ) }); this.Then(/^.* todo list should contain (.*?)$/, function (items: string) { return expect(actor.toSee(TodoList.Items_Displayed)).eventually.deep.equal(listOf(items)) }); 

Au début, le résultat semble un peu massif, mais à mesure que le système se développe, la quantité de code réutilisé augmentera et les modifications de mise en page et de page n'affecteront que les sélecteurs CSS et les tâches / questions de bas niveau, laissant les scripts et les tâches de haut niveau pratiquement inchangés.


Implémentations


Si nous parlons de bibliothèques, et non de tentatives de petites villes pour appliquer ce modèle, le plus populaire est Serenity BDD pour Java. Javascript / TypeScript utilise son propre port SerenityJS . Hors de la boîte, elle sait travailler avec le concombre et le moka.


Une recherche rapide a également produit une bibliothèque pour .NET - tranquire . Je ne peux rien dire d'elle, puisque je ne l'ai jamais rencontrée auparavant.


Lorsque vous utilisez Serenity et SerenityJS, vous pouvez utiliser l'utilitaire de génération de rapports .


Rapport avec photos

Prenez le code en utilisant Mocha:


 describe('Finding things to do', () => { describe('James can', () => { describe('remove filters so that the list', () => { it('shows all the items', () => Actor.named('James').attemptsTo( Start.withATodoListContaining([ 'Write some code', 'Walk the dog' ]), CompleteATodoItem.called('Write some code'), FilterItems.toShowOnly('Active'), FilterItems.toShowOnly('All'), Ensure.theListOnlyContains('Write some code', 'Walk the dog'), )); }); }); }); 

Le rapport contiendra comme statistiques générales:


et une ventilation des Ă©tapes avec des captures d'Ă©cran:


Les références


Quelques articles liés:


  1. Objets de page refactorisés: étapes solides vers le modèle de scénario / parcours
  2. Au-delà des objets de page: Automatisation des tests de nouvelle génération avec sérénité et modèle de scénario
  3. Serenity BDD et le modèle de scénario

Papiers:





Bibliothèques:


  1. Serenity BDD - Java
  2. Serenity JS - JavaScript / TypeScript
  3. tranquire - .NET

Veuillez écrire dans les commentaires si vous utilisez d'autres modèles / façons d'organiser le code des autotests.

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


All Articles