Drehbuch - Kein Seitenobjekt

Mit der Zeit wird es schwieriger, Änderungen an einem Produkt vorzunehmen, und es besteht ein wachsendes Risiko, nicht nur neue Funktionen freizugeben, sondern auch alte zu beschädigen. Anstatt das gesamte Projekt manuell zu überprüfen, versuchen sie häufig, diesen Prozess zu automatisieren. Wenn Sie mit Personen sprechen, die Schnittstellen testen und auf Konferenzen herumlaufen, wird deutlich, dass in der Welt des Webtests Selenium-Regeln und die überwiegende Mehrheit Page Object als Organisation von Code verwenden.


Aber für mich als Programmierer hat mir dieses Muster und der Code, den ich mit verschiedenen Teams gesehen habe, aus irgendeinem Grund nie gefallen - die Buchstaben SOLID klangen in meinem Kopf. Aber ich war bereit, mich damit abzufinden, dass Tester Code nach Belieben schreiben, da es wie vor etwa einem Jahr bei Angular Connect keine Alternativen gab. Ich hörte einen Bericht über das Testen von Angular-Anwendungen anhand des Drehbuchmusters. Jetzt möchte ich teilen.



Experimentelles Kaninchen


Zunächst eine kurze Beschreibung der verwendeten Werkzeuge. Als Beispiel für die Implementierung nehme ich SerenityJS aus dem Originalbericht mit TypeScript als Sprache für Testskripte.


Wir werden Experimente über die TODO-Anwendung durchführen - eine Anwendung zum Erstellen einer einfachen Liste von Aufgaben. Als Beispiele verwenden wir den Code von Jan Molak , dem Ersteller der Bibliothek (TypeScript-Code, daher ging das Highlight ein wenig).


Beginnen wir 2009


Zu diesem Zeitpunkt erschien Selenium WebDriver und die Leute begannen, es zu benutzen. Leider ist es in vielen Fällen aufgrund der Neuheit der Technologie und der mangelnden Programmiererfahrung falsch. Die Tests erwiesen sich als vollständig kopiert, nicht unterstützt und "zerbrechlich".


Das Ergebnis ist ein negatives Feedback und ein schlechter Ruf bei Autotests. Am Ende antwortete Simon Stewart, Schöpfer von Selenium WebDriver, in Abwesenheit in seinem Artikel Meine Selentests sind nicht stabil! . Die allgemeine Botschaft: "Wenn Ihre Tests zerbrechlich sind und nicht gut funktionieren, liegt dies nicht an Selen, sondern daran, dass die Tests selbst nicht sehr gut geschrieben sind."


Um zu verstehen, worum es geht, schauen wir uns das folgende Benutzerszenario an:


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 

Die naive Implementierung "auf der Stirn" sieht folgendermaßen aus:


 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); }); }; 

Wie Sie sehen können, wird hier eine API auf niedriger Ebene verwendet, Manipulationen mit dem DOM, Kopieren und Einfügen von CSS-Selektoren. Es ist klar, dass Sie in Zukunft auch bei einer kleinen Änderung der Benutzeroberfläche den Code an vielen Stellen ändern müssen.


Als Lösung musste Selen etwas bieten, das gut genug war, um solche Probleme zu beseitigen, und gleichzeitig Menschen zugänglich war, die wenig oder keine Erfahrung in der objektorientierten Programmierung hatten. Eine solche Lösung war Page Object, ein Muster zum Organisieren von Code.


Martin Fowler beschreibt es als ein Abstraktionsobjekt um eine HTML-Seite oder deren Fragment, mit dem Sie mit den Seitenelementen interagieren können, ohne den HTML-Code selbst zu berühren. Idealerweise sollte ein solches Objekt es dem Client-Code ermöglichen, alles zu tun und zu sehen, was eine Person tun und sehen kann.


Nachdem wir unser Beispiel gemäß diesem Muster umgeschrieben haben, haben wir Folgendes:


 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); }); }; 

Auf den ersten Blick sieht es gut aus - wir haben die Vervielfältigung beseitigt und die Kapselung hinzugefügt. Es gibt jedoch ein Problem: Sehr oft wachsen solche Klassen im Entwicklungsprozess zu enormen Größen.



Sie können versuchen, diesen Code in unabhängige Komponenten zu zerlegen. Wenn der Code jedoch einen solchen Zustand erreicht, ist er bereits so eng miteinander verbunden, dass er sehr schwierig sein kann und sowohl die Klasse selbst als auch den gesamten Clientcode (Testfälle), der diesen Code verwendet, neu schreiben muss das Objekt. Tatsächlich ist eine große Klasse keine Krankheit, sondern ein Symptom ( Codegeruch ). Das Hauptproblem ist die Verletzung des Einzelverantwortungsprinzips und des Open Closed-Prinzips . Das Seitenobjekt impliziert eine Beschreibung der Seite und aller Arten der Interaktion mit ihr in einer Entität, die nicht erweitert werden kann, ohne den Code zu ändern.


Für unsere TODO-Anwendungen zur Klassenverantwortung gelten folgende (Bilder aus dem Artikel ):
Verantwortung


Wenn Sie versuchen, diese Klasse basierend auf den Prinzipien von SOLID zu trennen, erhalten Sie ungefähr Folgendes:


Domänenmodell


Das nächste Problem, das wir mit Page Object haben, ist, dass es auf Seiten ausgeführt wird. Bei Abnahmetests werden häufig User Stories verwendet. Behavior Driven Development (BDD) und die Gherkin- Sprache passen perfekt zu diesem Modell. Es versteht sich, dass der Pfad des Benutzers zum Ziel (Szenario) wichtiger ist als die spezifische Implementierung. Wenn Sie beispielsweise in allen Tests ein Anmelde-Widget verwendet haben, müssen Sie beim Ändern der Anmeldemethode (das Formular wurde verschoben und auf Single Sign On umgestellt) alle Tests ändern. In besonders dynamischen Projekten kann dies zeitaufwändig und zeitaufwändig sein und das Team dazu ermutigen, Schreibtests zu verschieben, bis sich die Seiten stabilisiert haben (auch wenn bei Anwendungsfällen alles klar ist).


Um dieses Problem zu lösen, lohnt es sich, es von der anderen Seite zu betrachten. Wir führen folgende Konzepte ein:


  1. Rollen - für wen ist das alles?
  2. Ziele - warum sind sie (Benutzer) hier und was wollen sie erreichen?
  3. Aufgaben - Was müssen sie tun, um diese Ziele zu erreichen?
  4. Aktionen - Wie genau sollte ein Benutzer mit einer Seite interagieren, um eine Aufgabe abzuschließen?

Somit wird jedes Testskript de facto zu einem Skript für den Benutzer, das auf die Ausführung einer bestimmten User Story abzielt. Wenn Sie diesen Ansatz mit den oben beschriebenen Prinzipien der objektorientierten Programmierung kombinieren, entsteht ein Drehbuchmuster (oder User Journey- Muster). Seine Ideen und Prinzipien wurden erstmals 2007 vor PageObject geäußert.


Drehbuch


Wir schreiben unseren Code unter Verwendung der resultierenden Prinzipien neu.


James ist der Schauspieler, der unseren Benutzer in diesem Szenario spielen wird. Er weiß, wie man einen Browser benutzt.


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

James 'Ziel ist es, den ersten Punkt zu seiner Liste hinzuzufügen:


  Scenario: Adding an item to a list with other items 

Um dieses Ziel zu erreichen, benötigt James Folgendes:


  1. Beginnen Sie mit einer Liste mit mehreren Elementen.
  2. neuen Artikel hinzufügen.

Wir können dies in zwei Klassen unterteilen:


 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)); } } 

Für die Task Schnittstelle muss die performAs Methode definiert werden, an die der Akteur während der Ausführung übertragen wird. attemptsTo - Kombinatorfunktion, akzeptiert eine beliebige Anzahl von Mischvorgängen. Auf diese Weise können Sie eine Vielzahl von Sequenzen erstellen. Tatsächlich öffnet diese Aufgabe lediglich einen Browser auf der gewünschten Seite und fügt dort Elemente hinzu. Schauen wir uns nun die Aufgabe an, ein Element hinzuzufügen:


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

Interessanter ist hier, dass Aktionen auf niedriger Ebene angezeigt werden. Geben Sie Text in das Seitenelement ein und drücken Sie die Eingabetaste. TodoList ist alles, was vom "beschreibenden" Teil des TodoList übrig bleibt. Hier haben wir CSS-Selektoren:


 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, es bleibt zu überprüfen, ob nach allen Manipulationen die richtigen Informationen angezeigt werden. SerenityJS bietet die Schnittstelle Question<T> - eine Entität, die einen angezeigten Wert oder eine Liste von Werten Text.ofAll ( Text.ofAll im obigen Beispiel). Wie könnte man eine Frage implementieren, die den Text eines HTML-Elements zurückgibt:


 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) { } } 

Was wichtig ist, ist die Browserbindung optional. BrowseTheWeb ist nur eine Fähigkeit , mit der Sie mit dem Browser interagieren können. Sie können beispielsweise die RecieveEmails implementieren, mit der der Schauspieler Briefe lesen kann (zur Registrierung auf der Website).


Alles zusammen ergibt dieses Schema (von Jan Molak ):


und das folgende Szenario:


 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)) }); 

Das Ergebnis sieht zunächst etwas massiv aus, aber mit dem Wachstum des Systems nimmt die Menge des wiederverwendeten Codes zu, und Layout- und Seitenänderungen wirken sich nur auf CSS-Selektoren und Aufgaben / Fragen auf niedriger Ebene aus, sodass die Skripte und Aufgaben auf hoher Ebene praktisch unverändert bleiben.


Implementierungen


Wenn wir über Bibliotheken sprechen und nicht über Versuche in Kleinstädten, dieses Muster anzuwenden, ist Serenity BDD für Java das beliebteste. Javascript / TypeScript verwendet einen eigenen SerenityJS- Port. Nach dem Auspacken weiß sie, wie man mit Gurken und Mokka arbeitet.


Eine schnelle Suche ergab auch eine Bibliothek für .NET - tranquire . Ich kann nichts über sie sagen, da ich sie vorher noch nicht getroffen habe.


Wenn Sie Serenity und SerenityJS verwenden, können Sie das Dienstprogramm zur Berichterstellung verwenden .


Bericht mit Bildern

Nimm den Code mit Mokka:


 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'), )); }); }); }); 

Der Bericht enthält als allgemeine Statistik:


und eine Aufschlüsselung der Schritte mit Screenshots:


Referenzen


Einige verwandte Artikel:


  1. Überarbeitete Seitenobjekte: FESTE Schritte zum Drehbuch- / Reisemuster
  2. Jenseits von Seitenobjekten: Testautomatisierung der nächsten Generation mit Gelassenheit und dem Drehbuchmuster
  3. Serenity BDD und das Drehbuchmuster

Papiere:





Bibliotheken:


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

Bitte schreiben Sie in die Kommentare, wenn Sie andere Muster / Methoden zum Organisieren des Codes von Autotests verwenden.

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


All Articles