Con el tiempo, hacer cambios en cualquier producto se vuelve más difícil, y existe un riesgo creciente de no solo lanzar nuevas funciones, sino también romper las antiguas. A menudo, en lugar de verificar manualmente todo el proyecto, intentan automatizar este proceso. Si habla con personas que prueban interfaces y recorren conferencias, queda claro que Selenium gobierna en el mundo de las pruebas web, y la gran mayoría usa Page Object como la organización del código.
Pero para mí, como programador, por alguna razón nunca me gustó este patrón y el código que vi con diferentes equipos: las letras SOLID sonaron en mi cabeza. Pero estaba listo para aceptar el hecho de que los evaluadores escriben el código que quieran, debido a la falta de alternativas, como hace aproximadamente un año, en Angular Connect, escuché un informe sobre la prueba de aplicaciones angulares usando el patrón Screenplay. Ahora quiero compartir.

Conejo experimental
Primero, una breve descripción de las herramientas utilizadas. Como ejemplo de implementación, tomaré SerenityJS del informe original, con TypeScript como el lenguaje de los scripts de prueba.
Pondremos experimentos sobre la aplicación TODO , una aplicación para crear una lista simple de tareas. Como ejemplos, utilizaremos el código de Jan Molak , el creador de la biblioteca (código TypeScript, por lo que el resaltado fue un poco).
Comencemos en 2009
En este momento, apareció Selenium WebDriver, y la gente comenzó a usarlo. Desafortunadamente, en una gran cantidad de casos, debido a la novedad de la tecnología y la falta de experiencia en programación, está mal. Las pruebas resultaron ser completas de copiar y pegar, sin soporte y "frágil".
El resultado es una retroalimentación negativa y una mala reputación con las pruebas automáticas. Al final, Simon Stewart, creador de Selenium WebDriver, respondió en ausencia en su artículo ¡ Mis pruebas de selenio no son estables! . El mensaje general: "si sus pruebas son frágiles y no funcionan bien, esto no se debe a Selenium, sino a que las pruebas en sí mismas no están muy escritas".
Para comprender lo que está en juego, echemos un vistazo al siguiente escenario de usuario:
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
La implementación ingenua "en la frente" se verá así:
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); }); };
Como puede ver, aquí se usa una API de bajo nivel, manipulaciones con el DOM, copiar y pegar los selectores css. Está claro que en el futuro, incluso con un pequeño cambio en la interfaz de usuario, tendrá que cambiar el código en muchos lugares.
Como solución, Selenium tenía que ofrecer algo lo suficientemente bueno como para deshacerse de tales problemas, y al mismo tiempo accesible para personas con poca o ninguna experiencia en programación orientada a objetos. Tal solución fue Page Object, un patrón para organizar el código.
Martin Fowler lo describe como un objeto de abstracción alrededor de una página HTML o su fragmento, lo que le permite interactuar con los elementos de la página sin tocar el propio HTML. Idealmente, dicho objeto debería permitir que el código del cliente haga y vea todo lo que una persona puede hacer y ver.

Habiendo reescrito nuestro ejemplo de acuerdo con este patrón, tenemos lo siguiente:
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); }); };
A primera vista, se ve bien: nos deshicimos de la duplicación y agregamos encapsulación. Pero hay un problema: muy a menudo tales clases en el proceso de desarrollo crecen a tamaños enormes .

Puede intentar descomponer este código en componentes independientes, pero si el código alcanza dicho estado, ya está tan estrechamente acoplado que puede ser muy difícil y requerirá reescribir tanto la clase en sí como el código completo del cliente (casos de prueba) que usan esto El objeto. De hecho, una gran clase no es una enfermedad, sino un síntoma ( olor a código ). El principal problema es la violación del Principio de Responsabilidad Única y el Principio Abierto Cerrado . El objeto de página implica una descripción de la página y todas las formas de interactuar con ella en una entidad, que no se puede expandir sin cambiar su código.
Para nuestra clase TODO, las aplicaciones de responsabilidad son las siguientes (imágenes del artículo ):

Si intenta separar esta clase según los principios de SOLID, obtendrá algo como lo siguiente:

Modelo de dominio
El siguiente problema que tenemos con Page Object es que opera en páginas. En cualquier prueba de aceptación, es común usar historias de usuarios. Behavior Driven Development (BDD) y el lenguaje Gherkin se ajustan perfectamente a este modelo. Se entiende que la ruta del usuario hacia el objetivo (escenario) es más importante que la implementación específica. Por ejemplo, si utiliza un widget de inicio de sesión en todas las pruebas, al cambiar el método de inicio de sesión (el formulario se movió, cambió a Inicio de sesión único), tendrá que cambiar todas las pruebas. En proyectos particularmente dinámicos, esto puede llevar mucho tiempo y mucho tiempo y alentar al equipo a posponer las pruebas de escritura hasta que las páginas se estabilicen (incluso si todo está claro con los casos de uso).
Para resolver este problema, vale la pena mirarlo desde el otro lado. Introducimos los siguientes conceptos:
- Roles: ¿para quién es todo?
- Objetivos: ¿por qué están (usuarios) aquí y qué quieren lograr?
- Tareas: ¿qué necesitan hacer para lograr estos objetivos?
- Acciones: ¿cómo debe interactuar exactamente un usuario con una página para completar una tarea?
Por lo tanto, cada script de prueba se convierte, de hecho, en un script para el usuario, dirigido a la ejecución de una historia de usuario específica. Si combina este enfoque con los principios de programación orientada a objetos descritos anteriormente, el resultado será un patrón de Guión (o Viaje del usuario ). Sus ideas y principios se expresaron por primera vez en 2007, incluso antes de PageObject.
Guion
Reescribimos nuestro código usando los principios resultantes.
James es el actor que interpretará a nuestro usuario en este escenario. Él sabe cómo usar un navegador.
let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser));
El objetivo de James es agregar el primer elemento a su lista:
Scenario: Adding an item to a list with other items
Para lograr este objetivo, James necesita lo siguiente:
- comienza con una lista que contiene varios elementos,
- Añadir nuevo elemento.
Podemos dividir esto en dos clases:
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)); } }
La interfaz de la Task
requiere la definición del método performAs
, donde se transferirá al actor durante la ejecución. attemptsTo
- función combinador, acepta cualquier número de barajaduras. Por lo tanto, puede construir una variedad de secuencias. De hecho, todo lo que hace esta tarea es abrir un navegador en la página deseada y agregar elementos allí. Ahora veamos la tarea de agregar un elemento:
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); }
Aquí es más interesante, aparecen acciones de bajo nivel: ingrese el texto en el elemento de la página y presione Entrar. TodoList
es todo lo que queda de la parte "descriptiva" del Objeto de página, aquí tenemos selectores 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, queda por verificar que después de todas las manipulaciones se muestre la información correcta. SerenityJS ofrece la interfaz Question<T>
, una entidad que devuelve un valor mostrado o una lista de valores ( Text.ofAll
en el ejemplo anterior). ¿Cómo podría uno implementar una pregunta que devuelve el texto de un elemento 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) { } }
Lo que es importante, el enlace del navegador es opcional. BrowseTheWeb
es solo Ability , que le permite interactuar con el navegador. Puede implementar, por ejemplo, la capacidad RecieveEmails
, que le permite al actor leer cartas (para registrarse en el sitio).
Poniendo todo junto, obtenemos este esquema (de Jan Molak ):

y el siguiente escenario:
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)) });
Al principio, el resultado parece un poco masivo, pero a medida que el sistema crece, la cantidad de código reutilizado aumentará, y los cambios de diseño y página solo afectarán a los selectores css y las tareas / preguntas de bajo nivel, dejando los scripts y las tareas de alto nivel prácticamente sin cambios.
Implementaciones
Si hablamos de bibliotecas, y no de intentos de pequeñas ciudades de aplicar este patrón, el más popular es Serenity BDD para Java. Javascript / TypeScript utiliza su propio puerto SerenityJS . Fuera de la caja, ella sabe cómo trabajar con Pepino y Mocha.
Una búsqueda rápida también produjo una biblioteca para .NET: tranquilidad . No puedo decir nada sobre ella, ya que no la he visto antes.
Al usar Serenity y SerenityJS, puede usar la utilidad de generación de informes .
Informe con fotosToma el código usando 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'), )); }); }); });
El informe contendrá como estadísticas generales:

y un desglose de los pasos con capturas de pantalla:

Referencias
Algunos artículos relacionados:
- Objetos de página refactorizados: pasos sólidos al guión / patrón de viaje
- Más allá de los objetos de página: Automatización de pruebas de próxima generación con serenidad y el patrón de guión
- Serenity BDD y el patrón de guión
Papeles
Bibliotecas:
- Serenity BDD - Java
- Serenity JS - JavaScript / TypeScript
- tranquilidad - .NET
Escriba en los comentarios si utiliza otros patrones / formas de organizar el código de las pruebas automáticas.