Roteiro - Não é um objeto de página

Com o tempo, fazer alterações em qualquer produto se torna mais difícil, e há um risco crescente de não apenas lançar novos recursos, mas também quebrar os antigos. Freqüentemente, em vez de verificar manualmente todo o projeto, eles tentam automatizar esse processo. Se você conversar com pessoas que testam interfaces e andam por conferências, fica claro que, no mundo dos testes na Web, as regras do Selenium e a grande maioria usam o Page Object como organização do código.


Mas para mim, como programador, por alguma razão, nunca gostei desse padrão e código que vi com equipes diferentes - as letras SOLID soavam na minha cabeça. Mas eu estava pronto para aceitar o fato de que os testadores escrevem o código como desejam, devido à falta de alternativas, há cerca de um ano, no Angular Connect, ouvi um relatório sobre o teste de aplicativos Angular usando o padrão Screenplay. Agora eu quero compartilhar.



Coelho experimental


Primeiro, uma breve descrição das ferramentas utilizadas. Como exemplo de implementação, utilizarei o SerenityJS do relatório original, com o TypeScript como a linguagem dos scripts de teste.


Vamos colocar experimentos sobre o aplicativo TODO - um aplicativo para criar uma lista simples de tarefas. Como exemplos, usaremos o código de Jan Molak , o criador da biblioteca (código TypeScript, então o destaque foi um pouco).


Vamos começar em 2009


Nesse momento, o Selenium WebDriver apareceu e as pessoas começaram a usá-lo. Infelizmente, em um grande número de casos, devido à novidade da tecnologia e à falta de experiência em programação, isso está errado. Os testes foram copiados e colados completos, sem suporte e "frágeis".


O resultado é um feedback negativo e uma má reputação com autotestes. No final, Simon Stewart, criador do Selenium WebDriver, respondeu à revelia em seu artigo Meus testes de selênio não são estáveis! . A mensagem geral: "se seus testes são frágeis e não funcionam bem - não é por causa do Selenium, mas porque os testes em si não são muito escritos".


Para entender o que está em jogo, vamos dar uma olhada no seguinte cenário do usuário:


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 

A implementação ingênua "na testa" será assim:


 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 você pode ver, uma API de baixo nível é usada aqui, manipulações com o DOM, copiar e colar de seletores de css. É claro que, no futuro, mesmo com uma pequena alteração na interface do usuário, você precisará alterar o código em muitos lugares.


Como solução, o Selenium precisava oferecer algo bom o suficiente para se livrar desses problemas e, ao mesmo tempo, acessível a pessoas com pouca ou nenhuma experiência em programação orientada a objetos. Essa solução foi o Page Object, um padrão para organizar o código.


Martin Fowler o descreve como um objeto de abstração em torno de uma página HTML ou de seu fragmento, permitindo que você interaja com os elementos da página sem tocar no próprio HTML. Idealmente, esse objeto deve permitir que o código do cliente faça e veja tudo o que uma pessoa pode fazer e ver.


Após reescrever nosso exemplo de acordo com esse padrão, temos o seguinte:


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

À primeira vista, parece bom - nos livramos da duplicação e adicionamos encapsulamento. Mas há um problema: muitas vezes essas classes no processo de desenvolvimento crescem em tamanhos enormes .



Você pode tentar decompor esse código em componentes independentes, mas se o código atingiu esse estado, ele já está tão fortemente acoplado que pode ser muito difícil e exigirá reescrever a própria classe e todo o código do cliente (casos de teste) que usam esse código. o objeto De fato, uma classe enorme não é uma doença, mas um sintoma ( cheiro de código ). O principal problema é a violação do Princípio da Responsabilidade Única e do Princípio Aberto Fechado . O Objeto de Página implica uma descrição da página e todas as maneiras de interagir com ela em uma entidade, que não podem ser expandidas sem alterar seu código.


Para nossas aplicações de responsabilidade de classe TODO, são as seguintes (fotos do artigo ):
Responsabilidade


Se você tentar separar essa classe com base nos princípios do SOLID, obterá algo como o seguinte:


Modelo de domínio


O próximo problema que temos com o objeto de página é que ele opera em páginas. Em qualquer teste de aceitação, é comum usar histórias de usuários. O Behavior Driven Development (BDD) e a linguagem Gherkin se encaixam perfeitamente nesse modelo. Entende-se que o caminho do usuário para a meta (cenário) é mais importante que a implementação específica. Por exemplo, se você usar um widget de logon em todos os testes, ao alterar o método de logon (o formulário foi movido, alternado para Logon Único), será necessário alterar todos os testes. Em projetos particularmente dinâmicos, isso pode ser demorado e demorado e incentivar a equipe a adiar testes de gravação até que as páginas se estabilizem (mesmo que tudo esteja claro nos casos de uso).


Para resolver esse problema, vale a pena examiná-lo do outro lado. Introduzimos os seguintes conceitos:


  1. Papéis - para quem é tudo?
  2. Objetivos - por que eles (usuários) estão aqui e o que desejam alcançar?
  3. Tarefas - O que eles precisam fazer para alcançar esses objetivos?
  4. Ações - como exatamente um usuário deve interagir com uma página para concluir uma tarefa?

Assim, cada script de teste se torna, de fato, um script para o usuário, destinado à execução de uma história específica do usuário. Se você combinar essa abordagem com os princípios de programação orientada a objetos descritos acima, o resultado será um padrão de Roteiro (ou Viagem do Usuário ). Suas idéias e princípios foram expressados ​​pela primeira vez em 2007, mesmo antes do PageObject.


Roteiro


Reescrevemos nosso código usando os princípios resultantes.


James é o ator que interpretará nosso usuário nesse cenário. Ele sabe como usar um navegador.


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

O objetivo de James é adicionar o primeiro item à sua lista:


  Scenario: Adding an item to a list with other items 

Para atingir esse objetivo, James precisa do seguinte:


  1. comece com uma lista contendo vários itens,
  2. adicione novo item.

Podemos dividir isso em duas 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)); } } 

A interface Task requer a definição do método performAs , para o qual o ator será transferido durante a execução. attemptsTo função attemptsTo - combinator aceita qualquer número de shuffles. Assim, você pode construir uma variedade de sequências. De fato, tudo o que essa tarefa faz é abrir um navegador na página desejada e adicionar elementos nela. Agora, vejamos a tarefa de adicionar um 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); } // 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) } } 

É mais interessante aqui: ações de baixo nível são exibidas - insira o texto no elemento da página e pressione Enter. TodoList é tudo o que resta da parte "descritiva" do Objeto de Página, aqui temos seletores de 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, resta verificar se, após todas as manipulações, as informações corretas são exibidas. O SerenityJS oferece a interface Question<T> - uma entidade que retorna um valor exibido ou uma lista de valores ( Text.ofAll no exemplo acima). Como se poderia implementar uma pergunta que retorna o texto de um 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) { } } 

O que é importante, a ligação do navegador é opcional. BrowseTheWeb é apenas uma habilidade , que permite a você interagir com o navegador. Você pode implementar, por exemplo, a habilidade RecieveEmails , que permite ao ator ler cartas (para registro no site).


Juntando tudo, temos esse esquema (de Jan Molak ):


e o seguinte cenário:


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

No início, o resultado parece um pouco massivo, mas à medida que o sistema cresce, a quantidade de código reutilizado aumenta e as alterações de layout e página afetam apenas os seletores de css e as tarefas / perguntas de baixo nível, deixando os scripts e as tarefas de alto nível praticamente inalterados.


Implementações


Se falamos de bibliotecas, e não de tentativas de cidades pequenas de aplicar esse padrão, o mais popular é o Serenity BDD for Java. Javascript / TypeScript usa sua própria porta SerenityJS . Fora da caixa, ela sabe como trabalhar com Pepino e Mocha.


Uma pesquisa rápida também produziu uma biblioteca para o .NET - tranquire . Não posso dizer nada sobre ela, pois nunca a conheci antes.


Ao usar o Serenity e o SerenityJS, você pode usar o utilitário de geração de relatórios .


Relatório com fotos

Pegue o código usando o 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'), )); }); }); }); 

O relatório conterá como estatísticas gerais:


e um detalhamento das etapas com capturas de tela:


Referências


Alguns artigos relacionados:


  1. Objetos de página refatorados: etapas do SOLID para o padrão de roteiro / jornada
  2. Além dos objetos de página: automação de teste de próxima geração com serenidade e o padrão de roteiro
  3. Serenity BDD e o padrão de roteiro

Artigos:





Bibliotecas:


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

Por favor, escreva nos comentários se você usar outros padrões / maneiras de organizar o código dos autotestes.

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


All Articles