This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Diretrizes e recomendações

Guias e recomendações ao preparar soluções de testes com o projecto Selenium.

Uma nota sobre “Melhores práticas”: evitamos intencionalmente a frase “Melhores Práticas” nesta documentação. Nenhuma abordagem funciona para todas as situações. Preferimos a ideia de “Diretrizes e Recomendações”. Nós encorajamos que você leia e decida cuidadosamente quais abordagens funcionarão para você em seu ambiente específico.

O teste funcional é difícil de acertar por muitos motivos. Como se o estado, a complexidade e as dependências do aplicativo não tornassem o teste suficientemente difícil, lidar com navegadores (especialmente com incompatibilidades entre navegadores) torna a escrita de bons testes um desafio.

Selenium fornece ferramentas para facilitar a interação funcional do usuário, mas não o ajuda a escrever suítes de teste bem arquitetadas. Neste capítulo, oferecemos conselhos, diretrizes e recomendações sobre como abordar a automação funcional de páginas da web.

Este capítulo registra os padrões de design de software populares entre muitos dos usuários do Selenium que tiveram sucesso ao longo dos anos.

1 - Design patterns and development strategies

(previously located: https://github.com/SeleniumHQ/selenium/wiki/Bot-Style-Tests)

Overview

Over time, projects tend to accumulate large numbers of tests. As the total number of tests increases, it becomes harder to make changes to the codebase — a single “simple” change may cause numerous tests to fail, even though the application still works properly. Sometimes these problems are unavoidable, but when they do occur you want to be up and running again as quickly as possible. The following design patterns and strategies have been used before with WebDriver to help make tests easier to write and maintain. They may help you too.

DomainDrivenDesign: Express your tests in the language of the end-user of the app. PageObjects: A simple abstraction of the UI of your web app. LoadableComponent: Modeling PageObjects as components. BotStyleTests: Using a command-based approach to automating tests, rather than the object-based approach that PageObjects encourage

Loadable Component

What Is It?

The LoadableComponent is a base class that aims to make writing PageObjects less painful. It does this by providing a standard way of ensuring that pages are loaded and providing hooks to make debugging the failure of a page to load easier. You can use it to help reduce the amount of boilerplate code in your tests, which in turn make maintaining your tests less tiresome.

There is currently an implementation in Java that ships as part of Selenium 2, but the approach used is simple enough to be implemented in any language.

Simple Usage

As an example of a UI that we’d like to model, take a look at the new issue page. From the point of view of a test author, this offers the service of being able to file a new issue. A basic Page Object would look like:

package com.example.webdriver;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public class EditIssue {

  private final WebDriver driver;

  public EditIssue(WebDriver driver) {
    this.driver = driver;
  }

  public void setSummary(String summary) {
    WebElement field = driver.findElement(By.name("summary"));
    clearAndType(field, summary);
  }

  public void enterDescription(String description) {
    WebElement field = driver.findElement(By.name("comment"));
    clearAndType(field, description);
  }

  public IssueList submit() {
    driver.findElement(By.id("submit")).click();
    return new IssueList(driver);
  }

  private void clearAndType(WebElement field, String text) {
    field.clear();
    field.sendKeys(text);
  }
}

In order to turn this into a LoadableComponent, all we need to do is to set that as the base type:

public class EditIssue extends LoadableComponent<EditIssue> {
  // rest of class ignored for now
}

This signature looks a little unusual, but it all means is that this class represents a LoadableComponent that loads the EditIssue page.

By extending this base class, we need to implement two new methods:

  @Override
  protected void load() {
    driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
  }

  @Override
  protected void isLoaded() throws Error {
    String url = driver.getCurrentUrl();
    assertTrue("Not on the issue entry page: " + url, url.endsWith("/new"));
  }

The load method is used to navigate to the page, whilst the isLoaded method is used to determine whether we are on the right page. Although the method looks like it should return a boolean, instead it performs a series of assertions using JUnit’s Assert class. There can be as few or as many assertions as you like. By using these assertions it’s possible to give users of the class clear information that can be used to debug tests.

With a little rework, our PageObject looks like:

package com.example.webdriver;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

import static junit.framework.Assert.assertTrue;

public class EditIssue extends LoadableComponent<EditIssue> {

  private final WebDriver driver;
  
  // By default the PageFactory will locate elements with the same name or id
  // as the field. Since the summary element has a name attribute of "summary"
  // we don't need any additional annotations.
  private WebElement summary;
  
  // Same with the submit element, which has the ID "submit"
  private WebElement submit;
  
  // But we'd prefer a different name in our code than "comment", so we use the
  // FindBy annotation to tell the PageFactory how to locate the element.
  @FindBy(name = "comment") private WebElement description;
  
  public EditIssue(WebDriver driver) {
    this.driver = driver;
    
    // This call sets the WebElement fields.
    PageFactory.initElements(driver, this);
  }

  @Override
  protected void load() {
    driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
  }

  @Override
  protected void isLoaded() throws Error {
    String url = driver.getCurrentUrl();
    assertTrue("Not on the issue entry page: " + url, url.endsWith("/new"));
  }
  
  public void setSummary(String issueSummary) {
    clearAndType(summary, issueSummary);
  }

  public void enterDescription(String issueDescription) {
    clearAndType(description, issueDescription);
  }

  public IssueList submit() {
    submit.click();
    return new IssueList(driver);
  }

  private void clearAndType(WebElement field, String text) {
    field.clear();
    field.sendKeys(text);
  }
}

That doesn’t seem to have bought us much, right? One thing it has done is encapsulate the information about how to navigate to the page into the page itself, meaning that this information’s not scattered through the code base. It also means that we can do this in our tests:

EditIssue page = new EditIssue(driver).get();

This call will cause the driver to navigate to the page if that’s necessary.

Nested Components

LoadableComponents start to become more useful when they are used in conjunction with other LoadableComponents. Using our example, we could view the “edit issue” page as a component within a project’s website (after all, we access it via a tab on that site). You also need to be logged in to file an issue. We could model this as a tree of nested components:

 + ProjectPage
 +---+ SecuredPage
     +---+ EditIssue

What would this look like in code? For a start, each logical component would have its own class. The “load” method in each of them would “get” the parent. The end result, in addition to the EditIssue class above is:

ProjectPage.java:

package com.example.webdriver;

import org.openqa.selenium.WebDriver;

import static org.junit.Assert.assertTrue;

public class ProjectPage extends LoadableComponent<ProjectPage> {

  private final WebDriver driver;
  private final String projectName;

  public ProjectPage(WebDriver driver, String projectName) {
    this.driver = driver;
    this.projectName = projectName;
  }

  @Override
  protected void load() {
    driver.get("http://" + projectName + ".googlecode.com/");
  }

  @Override
  protected void isLoaded() throws Error {
    String url = driver.getCurrentUrl();

    assertTrue(url.contains(projectName));
  }
}

and SecuredPage.java:

package com.example.webdriver;

import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import static org.junit.Assert.fail;

public class SecuredPage extends LoadableComponent<SecuredPage> {

  private final WebDriver driver;
  private final LoadableComponent<?> parent;
  private final String username;
  private final String password;

  public SecuredPage(WebDriver driver, LoadableComponent<?> parent, String username, String password) {
    this.driver = driver;
    this.parent = parent;
    this.username = username;
    this.password = password;
  }

  @Override
  protected void load() {
    parent.get();

    String originalUrl = driver.getCurrentUrl();

    // Sign in
    driver.get("https://www.google.com/accounts/ServiceLogin?service=code");
    driver.findElement(By.name("Email")).sendKeys(username);
    WebElement passwordField = driver.findElement(By.name("Passwd"));
    passwordField.sendKeys(password);
    passwordField.submit();

    // Now return to the original URL
    driver.get(originalUrl);
  }

  @Override
  protected void isLoaded() throws Error {
    // If you're signed in, you have the option of picking a different login.
    // Let's check for the presence of that.

    try {
      WebElement div = driver.findElement(By.id("multilogin-dropdown"));
    } catch (NoSuchElementException e) {
      fail("Cannot locate user name link");
    }
  }
}

The “load” method in EditIssue now looks like:

  @Override
  protected void load() {
    securedPage.get();

    driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
  }

This shows that the components are all “nested” within each other. A call to get() in EditIssue will cause all its dependencies to load too. The example usage:

public class FooTest {
  private EditIssue editIssue;

  @Before
  public void prepareComponents() {
    WebDriver driver = new FirefoxDriver();

    ProjectPage project = new ProjectPage(driver, "selenium");
    SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret");
    editIssue = new EditIssue(driver, securedPage);
  }

  @Test
  public void demonstrateNestedLoadableComponents() {
    editIssue.get();

    editIssue.setSummary("Summary");
    editIssue.enterDescription("This is an example");
  }
}

If you’re using a library such as Guiceberry in your tests, the preamble of setting up the PageObjects can be omitted leading to nice, clear, readable tests.

Bot Pattern

(previously located: https://github.com/SeleniumHQ/selenium/wiki/Bot-Style-Tests)

Although PageObjects are a useful way of reducing duplication in your tests, it’s not always a pattern that teams feel comfortable following. An alternative approach is to follow a more “command-like” style of testing.

A “bot” is an action-oriented abstraction over the raw Selenium APIs. This means that if you find that commands aren’t doing the Right Thing for your app, it’s easy to change them. As an example:

public class ActionBot {
  private final WebDriver driver;

  public ActionBot(WebDriver driver) {
    this.driver = driver;
  }

  public void click(By locator) {
    driver.findElement(locator).click();
  }

  public void submit(By locator) {
    driver.findElement(locator).submit();
  }

  /** 
   * Type something into an input field. WebDriver doesn't normally clear these
   * before typing, so this method does that first. It also sends a return key
   * to move the focus out of the element.
   */
  public void type(By locator, String text) { 
    WebElement element = driver.findElement(locator);
    element.clear();
    element.sendKeys(text + "\n");
  }
}

Once these abstractions have been built and duplication in your tests identified, it’s possible to layer PageObjects on top of bots.

2 - Sobre automação de testes

Primeiro, comece perguntando a si mesmo se você realmente precisa ou não de um navegador. As probabilidades são de que, em algum ponto, se você estiver trabalhando em um aplicativo da web complexo, você precisará abrir um navegador e realmente testá-lo.

No entanto, os testes funcionais do usuário final, como os testes Selenium, são caros para executar. Além disso, eles normalmente exigem infraestrutura substancial para ser executado de forma eficaz. É uma boa regra sempre se perguntar se o que você deseja testar pode ser feito usando abordagens de teste mais leves, como testes de unidade ou com uma abordagem de nível inferior.

Depois de determinar que está no negócio de teste de navegador da web, e você tem seu ambiente Selenium pronto para começar a escrever testes, você geralmente executará alguma combinação de três etapas:

  • Configurar os dados
  • Executar um conjunto discreto de ações
  • Avaliar os resultados

Você deve manter essas etapas o mais curtas possível; uma ou duas operações devem ser suficientes na maioria das vezes. A automação do navegador tem a reputação de ser “instável”, mas, na realidade, é porque os usuários freqüentemente exigem muito dele. Em capítulos posteriores, retornaremos às técnicas que você pode usar para mitigar problemas aparentemente intermitentes nos testes, em particular sobre como superar as condições de corrida entre o navegador e o WebDriver.

Mantendo seus testes curtos e usando o navegador da web apenas quando você não tiver absolutamente nenhuma alternativa, você pode ter muitos testes com instabilidade mínima.

Uma vantagem distinta dos testes do Selenium é sua capacidade inerente de testar todos os componentes do aplicativo, de back-end para front-end, da perspectiva do usuário. Em outras palavras, embora os testes funcionais possam ser caros para executar, eles também abrangem grandes partes críticas para os negócios de uma só vez.

Requerimentos de teste

Como mencionado antes, os testes do Selenium podem ser caros para serem executados. Até que ponto depende do navegador em que você está executando os testes, mas historicamente o comportamento dos navegadores tem variado tanto que muitas vezes foi uma meta declarada testar cruzado contra vários navegadores.

Selenium permite que você execute as mesmas instruções em vários navegadores em vários sistemas operacionais, mas a enumeração de todos os navegadores possíveis, suas diferentes versões e os muitos sistemas operacionais em que são executados rapidamente se tornará uma tarefa não trivial.

Vamos começar com um exemplo

Larry escreveu um site que permite aos usuários solicitarem seus unicórnios personalizados.

O fluxo de trabalho geral (o que chamaremos de “caminho feliz”) é algo como isso:

  • Criar uma conta
  • Configurar o unicórnio
  • Adicionar ao carrinho de compras
  • Verificar e pagar
  • Dar feedback sobre o unicórnio

Seria tentador escrever um grande roteiro do Selenium para realizar todas essas operações - muitos tentarão. Resista à tentação! Isso resultará em um teste que a) leva muito tempo, b) estará sujeito a alguns problemas comuns em torno de problemas de tempo de renderização de página, e c) se falhar, não lhe dará um método conciso e “superficial” para diagnosticar o que deu errado.

A estratégia preferida para testar este cenário seria dividi-lo em uma série de testes independentes e rápidos, cada um dos quais tem uma “razão” de existir.

Vamos fingir que você deseja testar a segunda etapa: Configure o unicórnio. Ele executará as seguintes ações:

  • Criar uma conta
  • Configurar o unicórnio

Observe que estamos pulando o restante dessas etapas, vamos testar o resto do fluxo de trabalho em outros casos de teste pequenos e discretos depois de terminarmos com este.

Para começar, você precisa criar uma conta. Aqui você tem algumas escolhas a fazer:

  • Deseja usar uma conta existente?
  • Você deseja criar uma nova conta?
  • Existem propriedades especiais de tal usuário que precisam ser levadas em consideração antes do início da configuração?

Independentemente de como você responde a esta pergunta, a solução é torná-la parte da etapa de “configurar os dados” do teste. Se Larry expôs uma API que permite a você (ou qualquer pessoa) criar e atualizar contas de usuário, certifique-se de usar isso para responder a esta pergunta. Se possível, você deseja iniciar o navegador somente depois de ter um usuário “em mãos”, cujas credenciais você pode usar para fazer login.

Se cada teste para cada fluxo de trabalho começar com a criação de uma conta de usuário, muitos segundos serão adicionados à execução de cada teste. Chamar uma API e falar com um banco de dados são operações rápidas, “sem cabeçalho” que não requerem o processo caro de abrir um navegador, navegar para as páginas certas, clicando e aguardando o envio dos formulários, etc.

Idealmente, você pode abordar esta fase de configuração em uma linha de código, que será executado antes que qualquer navegador seja iniciado:

// Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
// mas eles não têm informações de pagamento configuradas, nem têm
// privilégios administrativos. No momento em que o usuário é criado, seu endereço
// de e-mail e senha são gerados aleatoriamente - você nem precisa
// conhecê-los.
User user = UserFactory.createCommonUser(); //Este método está definido em algum outro lugar.

// Faça login como este usuário.
// O login neste site leva você à sua página pessoal "Minha conta", e então
// o objeto AccountPage é retornado pelo método loginAs, permitindo que você
// execute ações da AccountPage.
AccountPage accountPage = loginAs(user.getEmail(), user.getPassword());
  
# Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
# mas eles não têm informações de pagamento configuradas, nem têm
# privilégios administrativos. No momento em que o usuário é criado, seu endereço
# de e-mail e senha são gerados aleatoriamente - você nem precisa
# conhecê-los.
user = user_factory.create_common_user() #This method is defined elsewhere.

# Faça login como este usuário.
# O login neste site leva você à sua página pessoal "Minha conta", e então
# o objeto AccountPage é retornado pelo método loginAs, permitindo que você
# execute ações da AccountPage.
account_page = login_as(user.get_email(), user.get_password())
  
// Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
// mas eles não têm informações de pagamento configuradas, nem têm
// privilégios administrativos. No momento em que o usuário é criado, seu endereço
// de e-mail e senha são gerados aleatoriamente - você nem precisa
// conhecê-los.
User user = UserFactory.CreateCommonUser(); //This method is defined elsewhere.

// Faça login como este usuário.
// O login neste site leva você à sua página pessoal "Minha conta", e então
// o objeto AccountPage é retornado pelo método loginAs, permitindo que você
// execute ações da AccountPage.
AccountPage accountPage = LoginAs(user.Email, user.Password);
  
# Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
# mas eles não têm informações de pagamento configuradas, nem têm
# privilégios administrativos. No momento em que o usuário é criado, seu endereço
# de e-mail e senha são gerados aleatoriamente - você nem precisa
# conhecê-los.
user = UserFactory.create_common_user #This method is defined elsewhere.

# Faça login como este usuário.
# O login neste site leva você à sua página pessoal "Minha conta", e então
# o objeto AccountPage é retornado pelo método loginAs, permitindo que você
# execute ações da AccountPage.
account_page = login_as(user.email, user.password)
  
// Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
// mas eles não têm informações de pagamento configuradas, nem têm
// privilégios administrativos. No momento em que o usuário é criado, seu endereço
// de e-mail e senha são gerados aleatoriamente - você nem precisa
// conhecê-los.
var user = userFactory.createCommonUser(); //This method is defined elsewhere.

// Faça login como este usuário.
// O login neste site leva você à sua página pessoal "Minha conta", e então
// o objeto AccountPage é retornado pelo método loginAs, permitindo que você
// execute ações da AccountPage.
var accountPage = loginAs(user.email, user.password);
  
// Crie um usuário que tenha permissões somente leitura - eles podem configurar um unicórnio,
// mas eles não têm informações de pagamento configuradas, nem têm
// privilégios administrativos. No momento em que o usuário é criado, seu endereço
// de e-mail e senha são gerados aleatoriamente - você nem precisa
// conhecê-los.
val user = UserFactory.createCommonUser() //This method is defined elsewhere.

// Faça login como este usuário.
// O login neste site leva você à sua página pessoal "Minha conta", e então
// o objeto AccountPage é retornado pelo método loginAs, permitindo que você
// execute ações da AccountPage.
val accountPage = loginAs(user.getEmail(), user.getPassword())
  

Como você pode imaginar, a UserFactory pode ser estendida para fornecer métodos como createAdminUser () e createUserWithPayment (). A questão é que essas duas linhas de código não o distraem do objetivo final deste teste: configurando um unicórnio.

Os detalhes do modelo de objeto de página será discutido em capítulos posteriores, mas vamos apresentar o conceito aqui:

Seus testes devem ser compostos de ações, realizadas do ponto de vista do usuário, dentro do contexto das páginas do site. Essas páginas são armazenadas como objetos, que conterão informações específicas sobre como a página da web é composta e como as ações são realizadas - muito pouco disso deve preocupar você como testador.

Que tipo de unicórnio você quer? Você pode querer rosa, mas não necessariamente. Roxo tem sido bastante popular ultimamente. Ela precisa de óculos escuros? Tatuagens de estrelas? Essas escolhas, embora difíceis, são sua principal preocupação como testador - você precisa garantir que seu centro de atendimento de pedidos envia o unicórnio certo para a pessoa certa, e isso começa com essas escolhas.

Observe que em nenhum lugar desse parágrafo falamos sobre botões, campos, menus suspensos, botões de opção ou formulários da web. Nem deveriam seus testes! Você deseja escrever seu código como o usuário tentando resolver seu problema. Aqui está uma maneira de fazer isso (continuando do exemplo anterior):

// O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
// Isso armazena apenas os valores; não preenche formulários da web nem interage
// com o navegador de qualquer forma.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
// lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
// nos leva lá.
AddUnicornPage addUnicornPage = accountPage.addUnicorn();

// Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
// o método createUnicorn(). Este método pegará os atributos do Sparkles,
// preencher o formulário e clicar em enviar.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);
  
# O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
# Isso armazena apenas os valores; não preenche formulários da web nem interage
# com o navegador de qualquer forma.
sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
# lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
# nos leva lá.
add_unicorn_page = account_page.add_unicorn()

# Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
# o método createUnicorn(). Este método pegará os atributos do Sparkles,
# preencher o formulário e clicar em enviar.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
// Isso armazena apenas os valores; não preenche formulários da web nem interage
// com o navegador de qualquer forma.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.Purple, UnicornAccessories.Sunglasses, UnicornAdornments.StarTattoos);

// Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
// lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
// nos leva lá.
AddUnicornPage addUnicornPage = accountPage.AddUnicorn();

// Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
// o método createUnicorn(). Este método pegará os atributos do Sparkles,
// preencher o formulário e clicar em enviar.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.CreateUnicorn(sparkles);
  
# O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
# Isso armazena apenas os valores; não preenche formulários da web nem interage
# com o navegador de qualquer forma.
sparkles = Unicorn.new('Sparkles', UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
# lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
# nos leva lá.
add_unicorn_page = account_page.add_unicorn

# Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
# o método createUnicorn(). Este método pegará os atributos do Sparkles,
# preencher o formulário e clicar em enviar.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
// Isso armazena apenas os valores; não preenche formulários da web nem interage
// com o navegador de qualquer forma.
var sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
// lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
// nos leva lá.
var addUnicornPage = accountPage.addUnicorn();

// Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
// o método createUnicorn(). Este método pegará os atributos do Sparkles,
// preencher o formulário e clicar em enviar.
var unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);

  
// O Unicórnio é um objeto de nível superior - ele possui atributos, que são definidos aqui.
// Isso armazena apenas os valores; não preenche formulários da web nem interage
// com o navegador de qualquer forma.
val sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

// Uma vez que já estamos "na" página da conta, temos que usá-la para chegar ao
// lugar real onde você configura os unicórnios. Chamar o método "Add Unicorn"
// nos leva lá.
val addUnicornPage = accountPage.addUnicorn()

// Agora que estamos na AddUnicornPage, passaremos o objeto "sparkles" para
// o método createUnicorn(). Este método pegará os atributos do Sparkles,
// preencher o formulário e clicar em enviar.
unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles)

  

Agora que você configurou seu unicórnio, você precisa passar para a etapa 3: certifique-se de que realmente funcionou.

// O método exists() de UnicornConfirmationPage pegará o objeto
// Sparkles - uma especificação dos atributos que você deseja ver e compará-los
// com os campos na página
Assert.assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles));
  
# O método exists() de UnicornConfirmationPage pegará o objeto
# Sparkles - uma especificação dos atributos que você deseja ver e compará-los
# com os campos na página
assert unicorn_confirmation_page.exists(sparkles), "Sparkles should have been created, with all attributes intact"
  
// O método exists() de UnicornConfirmationPage pegará o objeto
// Sparkles - uma especificação dos atributos que você deseja ver e compará-los
// com os campos na página
Assert.True(unicornConfirmationPage.Exists(sparkles), "Sparkles should have been created, with all attributes intact");
  
# O método exists() de UnicornConfirmationPage pegará o objeto
# Sparkles - uma especificação dos atributos que você deseja ver e compará-los
# com os campos na página
expect(unicorn_confirmation_page.exists?(sparkles)).to be, 'Sparkles should have been created, with all attributes intact'
  
// O método exists() de UnicornConfirmationPage pegará o objeto
// Sparkles - uma especificação dos atributos que você deseja ver e compará-los
// com os campos na página
assert(unicornConfirmationPage.exists(sparkles), "Sparkles should have been created, with all attributes intact");

  
// O método exists() de UnicornConfirmationPage pegará o objeto
// Sparkles - uma especificação dos atributos que você deseja ver e compará-los
// com os campos na página
assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles))
  

Observe que o testador ainda não fez nada além de falar sobre unicórnios neste código– sem botões, sem localizadores, sem controles do navegador. Este método de modelagem do aplicativo permite que você mantenha esses comandos de nível de teste no lugar e imutáveis, mesmo se Larry decidir na próxima semana que não gosta mais de Ruby-on-Rails e decidir reimplementar todo o site em Haskell com um front-end Fortran.

Seus objetos de página exigirão alguma pequena manutenção para estar conformidade com o redesenho do site, mas esses testes permanecerão os mesmos. Pegando esse design básico, você desejará continuar seus fluxos de trabalho com o menor número possível de etapas voltadas para o navegador. Seu próximo fluxo de trabalho envolverá adicionar um unicórnio ao carrinho de compras. Provavelmente, você desejará muitas iterações deste teste para ter certeza de que o carrinho está mantendo o estado adequado: Existe mais de um unicórnio no carrinho antes de você começar? Quantos cabem no carrinho de compras? Se você criar mais de um com o mesmo nome e / ou recursos, ele falhará? Manterá apenas o existente ou acrescentará outro?

Cada vez que você passa pelo fluxo de trabalho, você deseja evitar ter que criar uma conta, fazer login como o usuário e configurar o unicórnio. Idealmente, você será capaz de criar uma conta e pré-configurar um unicórnio por meio da API ou banco de dados. Em seguida, tudo que você precisa fazer é fazer login como o usuário, localizar Sparkles, e adicioná-lo ao carrinho.

Automatizar ou não automatizar?

A automação é sempre vantajosa? Quando se deve decidir automatizar os casos de teste?

Nem sempre é vantajoso automatizar casos de teste. Tem vezes que o teste manual pode ser mais apropriado. Por exemplo, se a interface do aplicativo mudará consideravelmente em um futuro próximo, então qualquer automação pode precisar ser reescrita de qualquer maneira. Além disso, às vezes simplesmente não há tempo suficiente para construir automação de testes. A curto prazo, o teste manual pode ser mais eficaz. Se um aplicativo tem um prazo muito curto, atualmente não há automação de teste disponível, e é imperativo que o teste seja feito dentro nesse período, o teste manual é a melhor solução.

3 - Tipos de teste

Teste de aceitação

Este tipo de teste é feito para determinar se um recurso ou sistema atende às expectativas e requisitos do cliente. Este tipo de teste geralmente envolve cooperação ou feedback do cliente, sendo uma atividade de validação que responde a pergunta:

Estamos construindo o produto certo?.

Para aplicações web, a automação desse teste pode ser feita diretamente com o Selenium, simulando o comportamento esperado do usuário. Esta simulação pode ser feita por gravação / reprodução ou por meio dos diferentes idiomas suportados, conforme explicado nesta documentação. Observação: o teste de aceitação é um subtipo de teste funcional, ao qual algumas pessoas também podem se referir.

Teste funcional

Este tipo de teste é feito para determinar se um recurso ou sistema funciona corretamente sem problemas. Verifica o sistema em diferentes níveis para garantir que todos os cenários são cobertos e que o sistema faz o que está suposto fazer. É uma atividade de verificação que responde a pergunta:

Estamos construindo o produto corretamente?.

Isso geralmente inclui: os testes funcionam sem erros (404, exceções …), de forma utilizável (redirecionamentos corretos), de forma acessível e atendendo às suas especificações (consulte teste de aceitação acima).

Para aplicativos da web, a automação desse teste pode ser feito diretamente com o Selenium, simulando os retornos esperados. Esta simulação pode ser feita por gravação / reprodução ou por meio de os diferentes idiomas suportados, conforme explicado nesta documentação.

Teste de performance/desempenho

Como o próprio nome indica, testes de desempenho são feitos para medir o desempenho de um aplicativo.

Existem dois subtipos principais para testes de desempenho:

Teste de carga

O teste de carga é feito para verificar o quão bem o aplicativo funciona sob diferentes cargas definidas (geralmente um determinado número de usuários conectados ao mesmo tempo).

Teste de estresse

O teste de estresse é feito para verificar o quão bem a aplicação funciona sob estresse (ou acima da carga máxima suportada).

Geralmente, os testes de estresse são feitos executando alguns testes escritos com Selenium simulando diferentes usuários utilizando uma função específica no aplicativo da web e recuperando algumas medições significativas.

Isso geralmente é feito por outras ferramentas que recuperam as métricas. Uma dessas ferramentas é a JMeter.

Para um aplicativo da web, os detalhes a serem medidos incluem taxa de transferência, latência, perda de dados, tempos de carregamento de componentes individuais …

Nota 1: todos os navegadores têm uma guia de desempenho em seus seção de ferramentas para desenvolvedores (acessível pressionando F12)

Nota 2: é um subtipo de teste não funcional já que isso geralmente é medido por sistema e não por função / recurso.

Teste regressivo

Esse teste geralmente é feito após uma alteração, correção ou adição de recurso.

Para garantir que a mudança não quebrou nenhumas das funcionalidades, alguns testes já executados são executados novamente.

O conjunto de testes re-executados pode ser total ou parcial e pode incluir vários tipos diferentes, dependendo da equipe de aplicação e desenvolvimento.

Desenvolvimento orientado a testes (TDD)

Em vez de um tipo de teste per se, o TDD é uma metodologia iterativa de desenvolvimento na qual os testes conduzem o design de um recurso.

Cada ciclo começa criando um conjunto de testes de unidade no qual o recurso deve eventualmente ser aprovado (eles devem falhar na primeira execução).

Depois disso, ocorre o desenvolvimento para fazer os testes passarem. Os testes são executados novamente, iniciando outro ciclo e esse processo continua até que todos os testes sejam aprovados.

Visa acelerar o desenvolvimento de um aplicativo com base no fato de que os defeitos custam menos quanto mais cedo são encontrados.

Desenvolvimento orientado a comportamento (BDD)

BDD também é uma metodologia de desenvolvimento iterativa com base no TDD acima, em que o objetivo é envolver todas as partes no desenvolvimento de um aplicativo.

Cada ciclo começa criando algumas especificações (que deve falhar). Em seguida, crie a os testes de unidade com falha (que também devem falhar) e, em seguida, faça o desenvolvimento.

Este ciclo é repetido até que todos os tipos de testes sejam aprovados.

Para fazer isso, uma linguagem de especificação é usada. Deve ser compreensível por todas as partes e ser simples, padronizada e explícita. A maioria das ferramentas usa Gherkin como esse idioma.

O objetivo é ser capaz de detectar ainda mais erros do que TDD, visando potenciais erros de aceitação também e tornar a comunicação entre as partes mais fácil.

Um conjunto de ferramentas está atualmente disponível para escrever as especificações e combiná-las com funções de código, como Cucumber ou SpecFlow.

Um conjunto de ferramentas é construído em cima do Selenium para tornar este processo ainda mais rápido, transformando diretamente as especificações BDD em código executável. Alguns deles são JBehave, Capybara e Robot Framework.

4 - Diretrizes e recomendações

Guias e recomendações ao preparar soluções de testes com o projecto Selenium.

Uma nota sobre “Melhores práticas”: evitamos intencionalmente a frase “Melhores Práticas” nesta documentação. Nenhuma abordagem funciona para todas as situações. Preferimos a ideia de “Diretrizes e Recomendações”. Nós encorajamos que você leia e decida cuidadosamente quais abordagens funcionarão para você em seu ambiente específico.

O teste funcional é difícil de acertar por muitos motivos. Como se o estado, a complexidade e as dependências do aplicativo não tornassem o teste suficientemente difícil, lidar com navegadores (especialmente com incompatibilidades entre navegadores) torna a escrita de bons testes um desafio.

Selenium fornece ferramentas para facilitar a interação funcional do usuário, mas não o ajuda a escrever suítes de teste bem arquitetadas. Neste capítulo, oferecemos conselhos, diretrizes e recomendações sobre como abordar a automação funcional de páginas da web.

Este capítulo registra os padrões de design de software populares entre muitos dos usuários do Selenium que tiveram sucesso ao longo dos anos.

4.1 - Modelos de objetos de página

Note: this page has merged contents from multiple sources, including the Selenium wiki

Overview

Within your web app’s UI, there are areas where your tests interact with. A Page Object only models these as objects within the test code. This reduces the amount of duplicated code and means that if the UI changes, the fix needs only to be applied in one place.

Page Object is a Design Pattern that has become popular in test automation for enhancing test maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your AUT. The tests then use the methods of this page object class whenever they need to interact with the UI of that page. The benefit is that if the UI changes for the page, the tests themselves don’t need to change, only the code within the page object needs to change. Subsequently, all changes to support that new UI are located in one place.

Advantages

  • There is a clean separation between the test code and page-specific code, such as locators (or their use if you’re using a UI Map) and layout.
  • There is a single repository for the services or operations the page offers rather than having these services scattered throughout the tests.

In both cases, this allows any modifications required due to UI changes to all be made in one place. Helpful information on this technique can be found on numerous blogs as this ‘test design pattern’ is becoming widely used. We encourage readers who wish to know more to search the internet for blogs on this subject. Many have written on this design pattern and can provide helpful tips beyond the scope of this user guide. To get you started, we’ll illustrate page objects with a simple example.

Examples

First, consider an example, typical of test automation, that does not use a page object:

/***
 * Tests login feature
 */
public class Login {

  public void testLogin() {
    // fill login data on sign-in page
    driver.findElement(By.name("user_name")).sendKeys("userName");
    driver.findElement(By.name("password")).sendKeys("my supersecret password");
    driver.findElement(By.name("sign-in")).click();

    // verify h1 tag is "Hello userName" after login
    driver.findElement(By.tagName("h1")).isDisplayed();
    assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
  }
}

There are two problems with this approach.

  • There is no separation between the test method and the AUT’s locators (IDs in this example); both are intertwined in a single method. If the AUT’s UI changes its identifiers, layout, or how a login is input and processed, the test itself must change.
  • The ID-locators would be spread in multiple tests, in all tests that had to use this login page.

Applying the page object techniques, this example could be rewritten like this in the following example of a page object for a Sign-in page.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Sign-in page.
 */
public class SignInPage {
  protected WebDriver driver;

  // <input name="user_name" type="text" value="">
  private By usernameBy = By.name("user_name");
  // <input name="password" type="password" value="">
  private By passwordBy = By.name("password");
  // <input name="sign_in" type="submit" value="SignIn">
  private By signinBy = By.name("sign_in");

  public SignInPage(WebDriver driver){
    this.driver = driver;
     if (!driver.getTitle().equals("Sign In Page")) {
      throw new IllegalStateException("This is not Sign In Page," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Login as valid user
    *
    * @param userName
    * @param password
    * @return HomePage object
    */
  public HomePage loginValidUser(String userName, String password) {
    driver.findElement(usernameBy).sendKeys(userName);
    driver.findElement(passwordBy).sendKeys(password);
    driver.findElement(signinBy).click();
    return new HomePage(driver);
  }
}

and page object for a Home page could look like this.

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Home Page
 */
public class HomePage {
  protected WebDriver driver;

  // <h1>Hello userName</h1>
  private By messageBy = By.tagName("h1");

  public HomePage(WebDriver driver){
    this.driver = driver;
    if (!driver.getTitle().equals("Home Page of logged in user")) {
      throw new IllegalStateException("This is not Home Page of logged in user," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Get message (h1 tag)
    *
    * @return String message text
    */
  public String getMessageText() {
    return driver.findElement(messageBy).getText();
  }

  public HomePage manageProfile() {
    // Page encapsulation to manage profile functionality
    return new HomePage(driver);
  }
  /* More methods offering the services represented by Home Page
  of Logged User. These methods in turn might return more Page Objects
  for example click on Compose mail button could return ComposeMail class object */
}

So now, the login test would use these two page objects as follows.

/***
 * Tests login feature
 */
public class TestLogin {

  @Test
  public void testLogin() {
    SignInPage signInPage = new SignInPage(driver);
    HomePage homePage = signInPage.loginValidUser("userName", "password");
    assertThat(homePage.getMessageText(), is("Hello userName"));
  }

}

There is a lot of flexibility in how the page objects may be designed, but there are a few basic rules for getting the desired maintainability of your test code.

Assertions in Page Objects

Page objects themselves should never make verifications or assertions. This is part of your test and should always be within the test’s code, never in an page object. The page object will contain the representation of the page, and the services the page provides via methods but no code related to what is being tested should be within the page object.

There is one, single, verification which can, and should, be within the page object and that is to verify that the page, and possibly critical elements on the page, were loaded correctly. This verification should be done while instantiating the page object. In the examples above, both the SignInPage and HomePage constructors check that the expected page is available and ready for requests from the test.

Page Component Objects

A page object does not necessarily need to represent all the parts of a page itself. The same principles used for page objects can be used to create “Page Component Objects” that represent discrete chunks of the page and can be included in page objects. These component objects can provide references to the elements inside those discrete chunks, and methods to leverage the functionality provided by them.

For example, a Product page has multiple products.

<!-- Products Page -->
<div class="header_container">
    <span class="title">Products</span>
</div>

<div class="inventory_list">
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
    <div class="inventory_item">
    </div>
</div>

Each product is a component of the Products page.

<!-- Inventory Item -->
<div class="inventory_item">
    <div class="inventory_item_name">Backpack</div>
    <div class="pricebar">
        <div class="inventory_item_price">$29.99</div>
        <button id="add-to-cart-backpack">Add to cart</button>
    </div>
</div>

The Product page HAS-A list of products. This relationship is called Composition. In simpler terms, something is composed of another thing.

public abstract class BasePage {
    protected WebDriver driver;

    public BasePage(WebDriver driver) {
        this.driver = driver;
    }
}

// Page Object
public class ProductsPage extends BasePage {
    public ProductsPage(WebDriver driver) {
        super(driver);
        // No assertions, throws an exception if the element is not loaded
        new WebDriverWait(driver, Duration.ofSeconds(3))
            .until(d -> d.findElement(By.className("header_container")));
    }

    // Returning a list of products is a service of the page
    public List<Product> getProducts() {
        return driver.findElements(By.className("inventory_item"))
            .stream()
            .map(e -> new Product(e)) // Map WebElement to a product component
            .toList();
    }

    // Return a specific product using a boolean-valued function (predicate)
    // This is the behavioral Strategy Pattern from GoF
    public Product getProduct(Predicate<Product> condition) {
        return getProducts()
            .stream()
            .filter(condition) // Filter by product name or price
            .findFirst()
            .orElseThrow();
    }
}

The Product component object is used inside the Products page object.

public abstract class BaseComponent {
    protected WebElement root;

    public BaseComponent(WebElement root) {
        this.root = root;
    }
}

// Page Component Object
public class Product extends BaseComponent {
    // The root element contains the entire component
    public Product(WebElement root) {
        super(root); // inventory_item
    }

    public String getName() {
        // Locating an element begins at the root of the component
        return root.findElement(By.className("inventory_item_name")).getText();
    }

    public BigDecimal getPrice() {
        return new BigDecimal(
                root.findElement(By.className("inventory_item_price"))
                    .getText()
                    .replace("$", "")
            ).setScale(2, RoundingMode.UNNECESSARY); // Sanitation and formatting
    }

    public void addToCart() {
        root.findElement(By.id("add-to-cart-backpack")).click();
    }
}

So now, the products test would use the page object and the page component object as follows.

public class ProductsTest {
    @Test
    public void testProductInventory() {
        var productsPage = new ProductsPage(driver);
        var products = productsPage.getProducts();
        assertEquals(6, products.size()); // expected, actual
    }
    
    @Test
    public void testProductPrices() {
        var productsPage = new ProductsPage(driver);

        // Pass a lambda expression (predicate) to filter the list of products
        // The predicate or "strategy" is the behavior passed as parameter
        var backpack = productsPage.getProduct(p -> p.getName().equals("Backpack"));
        var bikeLight = productsPage.getProduct(p -> p.getName().equals("Bike Light"));

        assertEquals(new BigDecimal("29.99"), backpack.getPrice());
        assertEquals(new BigDecimal("9.99"), bikeLight.getPrice());
    }
}

The page and component are represented by their own objects. Both objects only have methods for the services they offer, which matches the real-world application in object-oriented programming.

You can even nest component objects inside other component objects for more complex pages. If a page in the AUT has multiple components, or common components used throughout the site (e.g. a navigation bar), then it may improve maintainability and reduce code duplication.

Other Design Patterns Used in Testing

There are other design patterns that also may be used in testing. Some use a Page Factory for instantiating their page objects. Discussing all of these is beyond the scope of this user guide. Here, we merely want to introduce the concepts to make the reader aware of some of the things that can be done. As was mentioned earlier, many have blogged on this topic and we encourage the reader to search for blogs on these topics.

Implementation Notes

PageObjects can be thought of as facing in two directions simultaneously. Facing toward the developer of a test, they represent the services offered by a particular page. Facing away from the developer, they should be the only thing that has a deep knowledge of the structure of the HTML of a page (or part of a page) It’s simplest to think of the methods on a Page Object as offering the “services” that a page offers rather than exposing the details and mechanics of the page. As an example, think of the inbox of any web-based email system. Amongst the services it offers are the ability to compose a new email, choose to read a single email, and list the subject lines of the emails in the inbox. How these are implemented shouldn’t matter to the test.

Because we’re encouraging the developer of a test to try and think about the services they’re interacting with rather than the implementation, PageObjects should seldom expose the underlying WebDriver instance. To facilitate this, methods on the PageObject should return other PageObjects. This means we can effectively model the user’s journey through our application. It also means that should the way that pages relate to one another change (like when the login page asks the user to change their password the first time they log into a service when it previously didn’t do that), simply changing the appropriate method’s signature will cause the tests to fail to compile. Put another way; we can tell which tests would fail without needing to run them when we change the relationship between pages and reflect this in the PageObjects.

One consequence of this approach is that it may be necessary to model (for example) both a successful and unsuccessful login; or a click could have a different result depending on the app’s state. When this happens, it is common to have multiple methods on the PageObject:

public class LoginPage {
    public HomePage loginAs(String username, String password) {
        // ... clever magic happens here
    }
    
    public LoginPage loginAsExpectingError(String username, String password) {
        //  ... failed login here, maybe because one or both of the username and password are wrong
    }
    
    public String getErrorMessage() {
        // So we can verify that the correct error is shown
    }
}

The code presented above shows an important point: the tests, not the PageObjects, should be responsible for making assertions about the state of a page. For example:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    inbox.assertMessageWithSubjectIsUnread("I like cheese");
    inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}

could be re-written as:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
    assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}

Of course, as with every guideline, there are exceptions, and one that is commonly seen with PageObjects is to check that the WebDriver is on the correct page when we instantiate the PageObject. This is done in the example below.

Finally, a PageObject need not represent an entire page. It may represent a section that appears frequently within a site or page, such as site navigation. The essential principle is that there is only one place in your test suite with knowledge of the structure of the HTML of a particular (part of a) page.

Summary

  • The public methods represent the services that the page offers
  • Try not to expose the internals of the page
  • Generally don’t make assertions
  • Methods return other PageObjects
  • Need not represent an entire page
  • Different results for the same action are modelled as different methods

Example

public class LoginPage {
    private final WebDriver driver;

    public LoginPage(WebDriver driver) {
        this.driver = driver;

        // Check that we're on the right page.
        if (!"Login".equals(driver.getTitle())) {
            // Alternatively, we could navigate to the login page, perhaps logging out first
            throw new IllegalStateException("This is not the login page");
        }
    }

    // The login page contains several HTML elements that will be represented as WebElements.
    // The locators for these elements should only be defined once.
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // The login page allows the user to type their username into the username field
    public LoginPage typeUsername(String username) {
        // This is the only place that "knows" how to enter a username
        driver.findElement(usernameLocator).sendKeys(username);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;	
    }

    // The login page allows the user to type their password into the password field
    public LoginPage typePassword(String password) {
        // This is the only place that "knows" how to enter a password
        driver.findElement(passwordLocator).sendKeys(password);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;	
    }

    // The login page allows the user to submit the login form
    public HomePage submitLogin() {
        // This is the only place that submits the login form and expects the destination to be the home page.
        // A seperate method should be created for the instance of clicking login whilst expecting a login failure. 
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the login page ever
        // go somewhere else (for example, a legal disclaimer) then changing the method signature
        // for this method will mean that all tests that rely on this behaviour won't compile.
        return new HomePage(driver);	
    }

    // The login page allows the user to submit the login form knowing that an invalid username and / or password were entered
    public LoginPage submitLoginExpectingFailure() {
        // This is the only place that submits the login form and expects the destination to be the login page due to login failure.
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the user ever be navigated to the home page after submiting a login with credentials 
        // expected to fail login, the script will fail when it attempts to instantiate the LoginPage PageObject.
        return new LoginPage(driver);	
    }

    // Conceptually, the login page offers the user the service of being able to "log into"
    // the application using a user name and password. 
    public HomePage loginAs(String username, String password) {
        // The PageObject methods that enter username, password & submit login have already defined and should not be repeated here.
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}

Support in WebDriver

There is a PageFactory in the support package that provides support for this pattern and helps to remove some boiler-plate code from your Page Objects at the same time.

4.2 - Linguagem específica de domínio (DSL)

Uma linguagem específica de domínio (DSL) é um sistema que fornece ao usuário um meio expressivo de resolver um problema. Ele permite a um usuário interagir com o sistema em seus termos - não apenas na linguagem do programador.

Seus usuários, em geral, não se importam com a aparência do seu site. Eles não preocupam-se com a decoração, animações ou gráficos. Eles deseja usar seu sistema para empurrar seus novos funcionários através do processo com dificuldade mínima; eles querem reservar uma viagem para o Alasca; eles querem configurar e comprar unicórnios com desconto. Seu trabalho como testador deve chegar o mais perto possível de “capturar” essa mentalidade. Com isso em mente, começamos a “modelar” o aplicativo que você está trabalhando, de modo que os scripts de teste (o único proxy de pré-lançamento do usuário) “fala a linguagem” e representa o usuário.

Com Selenium, DSL é geralmente representado por métodos, escritos para fazer a API simples e legível - eles permitem um relatório entre o desenvolvedores e as partes interessadas (usuários, proprietários de produtos, negócios especialistas em inteligência, etc.).

Benefícios

  • Legível: As partes interessadas da empresa podem entendê-lo.
  • Gravável: Fácil de escrever, evita duplicações desnecessárias.
  • Extensível: Funcionalidade pode (razoavelmente) ser adicionada sem quebrar contratos e funcionalidades existentes.
  • Manutenção: Deixando os detalhes de implementação fora do teste casos, você está bem isolado contra alterações no AUT *.

Java

Aqui está um exemplo de um método DSL razoável em Java. Por questão de brevidade, ele assume que o objeto driver é pré-definido e está disponível para o método.

/**
 * Recebe um username e password, prrenche os campos, e clica em "login".
 * @return Uma instância de AccountPage
 */
public AccountPage loginAsUser(String username, String password) {
  WebElement loginField = driver.findElement(By.id("loginField"));
  loginField.clear();
  loginField.sendKeys(username);

  // Preenche o campo password. O localizador que estamos usando é "By.id", e devemos
  // definí-lo em algum outro lugar dentro da Classe.
  WebElement passwordField = driver.findElement(By.id("password"));
  passwordField.clear();
  passwordField.sendKeys(password);

  // Clica o botão de login, que possui o id "submit".
  driver.findElement(By.id("submit")).click();

  // Cria e retorna uma nova instância de AccountPage (via o Selenium
  // PageFactory embutido).
  return PageFactory.newInstance(AccountPage.class);
}

Este método abstrai completamente os conceitos de campos de entrada, botões, cliques e até páginas do seu código de teste. Usando este abordagem, tudo o que o testador precisa fazer é chamar esse método. Isto dá uma vantagem de manutenção: se os campos de login mudaram, você teria apenas que alterar esse método - não seus testes.

public void loginTest() {
    loginAsUser("cbrown", "cl0wn3");

    // Agora que estamos logados, fazemos alguma outra coisa--como usamos uma DSL para suportar
    // nossos testadores, é apenas escolher um dos métodos disponíveis.
    do.something();
    do.somethingElse();
    Assert.assertTrue("Algo deveria ter sido feito!", something.wasDone());

    // Note que ainda não nos referimos a nenhum botão ou web control nesse
    // script...
}

Vale a pena repetir: um de seus principais objetivos deve ser escrever um API que permite que seus testes resolvam o problema em questão, e NÃO o problema da IU. A IU é uma preocupação secundária para o seu usuários - eles não se importam com a interface do usuário, eles apenas querem fazer seu trabalho feito. Seus scripts de teste devem ser lidos como uma lista de itens sujos que o usuário deseja FAZER e as coisas que deseja SABER. Os testes não devem se preocupar com COMO a interface do usuário exige que você vá sobre isso.

*AUT: Application under test

4.3 - Gerando estado da aplicação

Selenium não deve ser usado para preparar um caso de teste. Tudo as ações repetitivas e preparações para um caso de teste devem ser feitas por meio de outros métodos. Por exemplo, a maioria das IUs da web tem autenticação (por exemplo, um formulário de login). Eliminar o login via navegador da web antes de cada teste irá melhorar a velocidade e estabilidade do teste. Um método deve ser criado para obter acesso à AUT* (por exemplo, usando uma API para fazer login e definir um cookie). Além disso, a criação de métodos para pré-carregar dados para o teste não deve ser feito usando Selenium. Como dito anteriormente, APIs existentes devem ser aproveitadas para criar dados para a AUT *.

*AUT: Application under test

4.4 - Simulação de serviços externos

Eliminar as dependências de serviços externos melhorará muito a velocidade e estabilidade de seus testes.

4.5 - Relatórios melhorados

O Selenium não foi projetado para relatar sobre o status de casos de teste. Aproveitar os recursos de relatórios integrados de frameworks de teste unitários é um bom começo. A maioria dos frameworks de teste unitários podem gerar relatórios formatados em xUnit ou HTML. Relatórios xUnit são populares para importar resultados para um servidor de integração contínua (CI) como Jenkins, Travis, Bamboo, etc. Aqui estão alguns links para obter mais informações sobre resultados de relatórios em vários idiomas.

NUnit 3 Console Runner

NUnit 3 Console Command Line

xUnit getting test results in TeamCity

xUnit getting test results in CruiseControl.NET

xUnit getting test results in Azure DevOps

4.6 - Evite compartilhamento de estado

Embora mencionado em vários lugares, vale a pena mencionar novamente. Garanta que os testes são isolados uns dos outros.

  • Não compartilhe dados de teste. Imagine vários testes em que cada um consulta o banco de dados para pedidos válidos antes de escolher um para executar uma ação. Caso dois testes peguem a mesma ordem, provavelmente você obterá um comportamento inesperado.

  • Limpe dados desatualizados no aplicativo que podem ser obtidos por outro teste, por exemplo registros de pedidos inválidos.

  • Crie uma nova instância do WebDriver por teste. Isso ajuda a garantir o isolamento do teste e torna a paralelização mais simples.

4.7 - Tips on working with locators

When to use which locators and how best to manage them in your code.

Take a look at examples of the supported locator strategies.

No geral, se os IDs de HTML estiverem disponíveis, únicos e consistentemente previsíveis, eles são o método preferido para localizar um elemento uma página. Eles tendem a trabalhar muito rapidamente e dispensar muito processamento que vem com travessias de DOM complicadas.

Se IDs exclusivos não estiverem disponíveis, um seletor CSS bem escrito é o método preferido de localização de um elemento. XPath funciona bem como CSS seletores, mas a sintaxe é complicada e frequentemente difícil de depurar. Embora os seletores XPath sejam muito flexíveis, eles não são tipicamente testados em performance por fornecedores de navegadores e tendem a ser bastante lentos.

As estratégias de seleção baseadas em linkText e partialLinkText têm desvantagens porque eles só funcionam em elementos de link. Além disso, eles chamam seletores querySelectorAll internamente no WebDriver.

O nome da tag pode ser uma maneira perigosa de localizar elementos. tem frequentemente, vários elementos da mesma tag presentes na página. Isso é útil principalmente ao chamar o método _findElements(By) _ que retorna uma coleção de elementos.

A recomendação é manter seus localizadores compactos e legíveis quanto possível. Pedir ao WebDriver para percorrer a estrutura DOM é uma operação cara, e quanto mais você pode restringir o escopo de sua pesquisa, melhor.

4.8 - Independência de Testes

Escreva cada teste como sua própria unidade. Escreva os testes de uma forma que não seja dependente de outros testes para concluir:

Digamos que existe um sistema de gerenciamento de conteúdo com o qual você pode criar algum conteúdo personalizado que então aparece em seu site como um módulo após publicação, e pode levar algum tempo para sincronizar entre o CMS e a aplicação.

Uma maneira errada de testar seu módulo é que o conteúdo seja criado e publicado em um teste e, em seguida, verificar o módulo em outro teste. Este teste não é viável, pois o conteúdo pode não estar disponível imediatamente para o outro teste após a publicação.

Em vez disso, você pode criar um conteúdo stub que pode ser ligado e desligado dentro do teste e use-o para validar o módulo. Contudo, para a criação de conteúdo, você ainda pode ter um teste separado.

4.9 - Considere usar uma API fluente

Martin Fowler cunhou o termo “API Fluent”. Selenium já implementa algo assim em sua classe FluentWait, que é pretende ser uma alternativa à classe padrão Wait. Você pode habilitar o padrão de design de API fluente em seu objeto de página e, em seguida, consulte a página de pesquisa do Google com um snippet de código como este:

driver.get( "http://www.google.com/webhp?hl=en&amp;tab=ww" );
GoogleSearchPage gsp = new GoogleSearchPage(driver);
gsp.setSearchString().clickSearchButton();

A classe de objeto da página do Google com este comportamento fluente pode ser assim:

public abstract class BasePage {
    protected WebDriver driver;

    public BasePage(WebDriver driver) {
        this.driver = driver;
    }
}

public class GoogleSearchPage extends BasePage {
    public GoogleSearchPage(WebDriver driver) {
        super(driver);
        // Generally do not assert within pages or components.
        // Effectively throws an exception if the lambda condition is not met.
        new WebDriverWait(driver, Duration.ofSeconds(3)).until(d -> d.findElement(By.id("logo")));
    }

    public GoogleSearchPage setSearchString(String sstr) {
        driver.findElement(By.id("gbqfq")).sendKeys(sstr);
        return this;
    }

    public void clickSearchButton() {
        driver.findElement(By.id("gbqfb")).click();
    }
}

4.10 - Navegador novo por teste

Comece cada teste a partir de um estado limpo conhecido. Idealmente, ligue uma nova máquina virtual para cada teste. Se ligar uma nova máquina virtual não for prático, pelo menos inicie um novo WebDriver para cada teste. Most browser drivers like GeckoDriver and ChromeDriver will start with a clean known state with a new user profile, by default.

WebDriver driver = new FirefoxDriver();

5 - Piores práticas

Temas a evitar quando automatizar navegadores com Selenium.

5.1 - Captchas

CAPTCHA, abreviação de Completely Automated Public Turing test to tell Computers and Humans Apart, foi projetado explicitamente para impedir a automação, portanto, não tente! Existem duas estratégias principais para contornar as verificações CAPTCHA:

  • Desative CAPTCHAs em seu ambiente de teste
  • Adicione um hook para permitir que os testes ignorem o CAPTCHA

5.2 - Downloads de arquivo

Embora seja possível iniciar um download clicando em um link com um navegador sob o controle do Selenium, a API não expõe o progresso do download, tornando-o menos do que ideal para testar arquivos baixados. Isso ocorre porque o download de arquivos não é considerado um aspecto importante de emular a interação do usuário com a plataforma da web. Em vez disso, encontre o link usando Selenium (e todos os cookies necessários) e passe este cookie para uma biblioteca de solicitação HTTP como libcurl.

O driver HtmlUnit pode baixar anexos acessando-os como fluxos de entrada, implementando o AttachmentHandler. O AttachmentHandler pode ser adicionado ao WebClient HtmlUnit.

5.3 - Códigos de respostas HTTP

Para algumas configurações de navegador no Selenium RC, Selenium atuou como um proxy entre o navegador e o site sendo automatizado. Isso significa que todo o tráfego do navegador que passou pelo Selenium poderia ser capturado ou manipulado. O método captureNetworkTraffic() pretendia capturar todo o tráfego de rede entre o navegador e o site sendo automatizado, incluindo códigos de resposta HTTP.

Selenium WebDriver é uma abordagem completamente diferente para a automação do navegador, preferindo agir mais como um usuário. Isso é representado na maneira como você escreve testes com o WebDriver. Em testes funcionais automatizados, verificar o código de status não é um detalhe particularmente importante da falha de um teste; as etapas que o precederam são mais importantes.

O navegador sempre representará o código de status HTTP, imagine, por exemplo, uma página de erro 404 ou 500. Uma maneira simples de “falhar rapidamente” quando você encontrar uma dessas páginas de erro é verificar o título da página ou o conteúdo de um ponto confiável (por exemplo, a tag <h1>) após cada carregamento de página. Se você estiver usando o modelo de objeto de página, você pode incluir esta verificação em seu construtor de classe ou ponto semelhante onde o carregamento da página é esperado. Ocasionalmente, o código HTTP pode até ser representado na página de erro do navegador e você pode usar o WebDriver para ler isso e melhorar sua saída de depuração.

Verificar se a própria página da web está alinhada com a prática ideal do WebDriver de representar a visão do usuário do site.

Se você insiste, uma solução avançada para capturar códigos de status HTTP é replicar o comportamento do Selenium RC usando um proxy. A API WebDriver fornece a capacidade de definir um proxy para o navegador, e há uma série de proxies que irão permitir que você manipule de forma programática o conteúdo das solicitações enviadas e recebidas do servidor da web. Usar um proxy permite que você decida como deseja responder para códigos de resposta de redirecionamento. Além disso, nem todo navegador torna os códigos de resposta disponíveis para WebDriver, então optar por usar um proxy permite que você tenha uma solução que funciona para todos os navegadores.

5.4 - Login via Gmail, email e Facebook

Por vários motivos, fazer login em sites como Gmail e Facebook usando do WebDriver não é recomendado. Além de ser contra os termos de uso desses sites (onde você corre o risco de ter a conta encerrada), é lento e não confiável.

A prática ideal é usar as APIs que os provedores de e-mail oferecem, ou no caso do Facebook, o serviço de ferramentas para desenvolvedores que expõe uma API para criar contas de teste, amigos e assim por diante. Embora usar uma API possa parecer um pouco trabalhoso, você será recompensado em velocidade, confiabilidade e estabilidade. A API também não deve mudar, enquanto as páginas da web e os localizadores de HTML mudam frequentemente e exigem que você atualize sua estrutura de teste.

Login em sites de terceiros usando WebDriver em qualquer ponto do seu teste aumenta o risco de seu teste falhar porque torna o teste mais longo. Uma regra geral é que testes mais longos são mais frágeis e não confiáveis.

Implementações WebDriver que estão em conformidade com W3C também anotam o objeto navigator com uma propriedade WebDriver para que os ataques de negação de serviço possam ser mitigados.

5.5 - Dependência entre testes

Uma ideia comum e um equívoco sobre o teste automatizado é sobre uma ordem de testes específica. Seus testes devem ser executados em qualquer ordem, e não depender da conclusão de outros testes para ter sucesso.

5.6 - Teste de performance/desempenho

Teste de desempenho usando Selenium e WebDriver geralmente não é recomendado. Não porque é incapaz, mas porque não é otimizado para o trabalho e é improvável que você obtenha bons resultados.

Pode parecer ideal para teste de desempenho no contexto do usuário, mas um conjunto de testes WebDriver estão sujeitos a muitos pontos de fragilidade externa e interna que estão além do seu controle; por exemplo, velocidade de inicialização do navegador, velocidade dos servidores HTTP, resposta de servidores de terceiros que hospedam JavaScript ou CSS, e a penalidade de instrumentação da própria implementação do WebDriver. A variação nesses pontos causará variação em seus resultados. É difícil separar a diferença entre o desempenho do seu site e o desempenho de recursos externos, e também é difícil dizer qual é a penalidade de desempenho para usar WebDriver no navegador, especialmente se você estiver injetando scripts.

A outra atração potencial é “economizar tempo” - execução de testes funcionais e de desempenho ao mesmo tempo. No entanto, os testes funcionais e de desempenho têm objetivos opostos. Para testar a funcionalidade, um testador pode precisar ser paciente e aguarde o carregamento, mas isso irá turvar os resultados do teste de desempenho e vice-versa.

Para melhorar o desempenho do seu site, você precisará ser capaz de analisar o desempenho geral independente das diferenças de ambiente, identificar práticas de código ruins, repartição do desempenho de recursos individuais (ou seja, CSS ou JavaScript), para saber o que melhorar. Existem ferramentas de teste de desempenho disponíveis que podem fazer este trabalho, que fornecem relatórios e análises, e podem até fazer sugestões de melhorias.

Pacotes de exemplo (código aberto) a serem usados ​​são: JMeter

5.7 - Navegação por links

Usar o WebDriver para navegar por links não é uma prática recomendada. Não porque não pode ser feito, mas porque WebDriver definitivamente não é a ferramenta ideal para isso. O WebDriver precisa de tempo para inicializar, e pode levar vários segundos, até um minuto dependendo de como seu teste é escrito, apenas para chegar à página e atravessar o DOM.

Em vez de usar o WebDriver para isso, você poderia economizar muito tempo executando um comando curl, ou usando uma biblioteca como BeautifulSoup uma vez que esses métodos não dependem em criar um navegador e navegar para uma página. Você está economizando muito tempo por não usar o WebDriver para essa tarefa.

5.8 - Autenticação de Dois Fatores (2FA)

A autenticação de dois fatores, conhecida como 2FA, é um mecanismo de autorização onde a senha de uso único (OTP) é gerada usando aplicativos móveis “Autenticadores”, como “Google Authenticator”, “Microsoft Authenticator” etc., ou por SMS, e-mail para autenticação. Automatizar isso perfeitamente e consistentemente é um grande desafio no Selenium. Existem algumas maneiras para automatizar este processo. Mas essa será outra camada em cima de nossos testes Selenium e não protegidos também. Portanto, você pode evitar a automação do 2FA.

Existem algumas opções para contornar as verificações 2FA:

  • Desative 2FA para determinados usuários no ambiente de teste, para que você possa usar essas credenciais de usuário na automação.
  • Desative 2FA em seu ambiente de teste.
  • Desative 2FA se você fizer o login de determinados IPs. Dessa forma, podemos configurar nosso teste os IPs da máquina para evitar isso.