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

Nota: esta página reuniu conteúdos de várias fontes, incluindo o Selenium wiki

Visão geral

Dentro da interface de usuário (UI) do seu aplicativo web, existem áreas com as quais seus testes interagem. O Page Object modela apenas essas áreas como objetos dentro do código de teste. Isso reduz a quantidade de código duplicado e significa que, se a UI mudar, a correção precisará ser aplicada apenas em um lugar.

Page Object é um padrão de design (Design Pattern) que se tornou popular na automação de testes para melhorar a manutenção de testes e reduzir a duplicação de código. Page Object é uma classe orientada a objetos que serve como interface para uma página do seu AUT (Aplicativo Sob Teste). Os testes usam então os métodos desta classe de Page Object sempre que precisam interagir com a UI dessa página. A vantagem é que, se a UI da página mudar, os próprios testes não precisam mudar, apenas o código dentro do Page Object precisa mudar. Subsequentemente, todas as mudanças para suportar essa nova UI estão localizadas em um lugar.

Vantagens

  • Existe uma separação bem definida entre o código do teste e o código da página especifica.
  • Existe um repositório único para os serviços ou operações que a página oferece, em vez de ter esses serviços espalhados pelos testes.

Em ambos os casos, isso permite que quaisquer modificações necessárias devido a mudanças na UI sejam feitas em um lugar somente. Informações úteis sobre esta técnica podem ser encontradas em vários blogs, pois este ‘padrão de design de teste (test design pattern)’ está se tornando amplamente utilizado. Encorajamos os leitores que desejam saber mais a pesquisar na internet por blogs sobre este assunto. Muitos já escreveram sobre este padrão de design e podem fornecer dicas úteis além do escopo deste guia do usuário. Para começar, vamos ilustrar Page Object com um exemplo simples.

Exemplos

Primeiro, considere um exemplo, típico da automação de testes, que não usa um objeto de página:

/***
 * Testes da funcionalidade de login
 */
public class Login {

  public void testLogin() {
    // preencha os dados de login na página de entrada
    driver.findElement(By.name("user_name")).sendKeys("userName");
    driver.findElement(By.name("password")).sendKeys("my supersecret password");
    driver.findElement(By.name("sign-in")).click();

    // verifique se a tag h1 é "Hello userName" após o login
    driver.findElement(By.tagName("h1")).isDisplayed();
    assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
  }
}

Existem dois problemas com essa abordagem.

  • Não há separação entre o método de teste e os localizadores do aplicativo em teste (IDs neste exemplo); ambos estão entrelaçados em um único método. Se a UI do aplicativo em teste muda seus identificadores, layout ou como um login é inserido e processado, o próprio teste deve mudar.
  • Os localizadores ID estariam espalhados em vários testes, em todos os testes que tivessem que usar esta página de login.

Aplicando as técnicas de Page Object, este exemplo poderia ser reescrito da seguinte forma no exemplo para uma página de login.

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

/**
 * Page Object encapsula a página de login.
 */
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());
    }
  }

  /**
    * Faz login como um usuário válido
    *
    * @param userName
    * @param password
    * @return pbjeto da Pagina Inicial
    */
  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);
  }
}

E o objeto de página para uma página inicial poderia parecer assim.

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

/**
 * Page Object encapsula a Página Inicial
 */
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());
    }
  }

  /**
    * Obtém a mensagem (tag h1)
    *
    * @return String da mensagem de texto
    */
  public String getMessageText() {
    return driver.findElement(messageBy).getText();
  }

  public HomePage manageProfile() {
    // Encapsulamento da página para gerenciar a funcionalidade do perfil
    return new HomePage(driver);
  }
  /* Mais métodos que oferecem os serviços representados pela Página inicial do usuário logado. Estes métodos por sua vez podem retornar mais Page Object, por exemplo, clicar no botão "Compor email" pode retornar um objeto da classe ComposeMail */
}

Então agora, o teste de login usaria esses dois objetos de página da seguinte maneira.

/***
 * Testes da funcionalidade de login
 */
public class TestLogin {

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

}

Há muita flexibilidade em como o Page Object pode ser projetado, mas existem algumas regras básicas para obter a manutenibilidade desejada do seu código de teste.

Afirmações em Page Objects

Os Page Objects em si nunca devem fazer verificações ou afirmações. Isso faz parte do seu teste e sempre deve estar dentro do código do teste, nunca em um objeto de página. O objeto de página conterá a representação da página e os serviços que a página fornece por meio de métodos, mas nenhum código relacionado ao que está sendo testado deve estar dentro do objeto de página.

Há uma única verificação que pode e deve estar dentro do objeto de página, e isso é para verificar se a página e possivelmente elementos críticos na página, foram carregados corretamente. Essa verificação deve ser feita ao instanciar o objeto de página. Nos exemplos acima, tanto os construtores SignInPage quanto HomePage verificam se a página esperada está disponível e pronta para solicitações do teste.

Objetos Componentes de Página (Page Component Object)

Page Object não precisa necessariamente representar todas as partes de uma página. Isso foi notado por Martin Fowler nos primeiros dias, enquanto cunhava o termo “objetos de painel (panel objects)”.

Os mesmos princípios usados para objetos de página podem ser usados para criar “Objetos Componente de Página”, como foi chamado mais tarde, que representam partes discretas da página e podem ser incluídos em Page Object. Esses objetos de componente podem fornecer referências aos elementos dentro dessas partes discretas e métodos para aproveitar a funcionalidade ou comportamento fornecidos por eles.

Por exemplo, uma página de Produtos tem vários produtos.

<!-- Página de Produtos -->
<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>

Cada produto é um componente da página de Produtos.

<!-- 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>

A página de Produtos “TEM-UMA (HAS-A)” lista de produtos. This object relationship is called Composition. Essa relação de objeto é chamada de Composição. Em termos mais simples, algo é composto de outra coisa.

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);
        // Sem afirmações, lança uma exceção se o elemento não for carregado
        new WebDriverWait(driver, Duration.ofSeconds(3))
            .until(d -> d.findElement(By.className("header_container")));
    }

    // Retornar uma lista de produtos é um serviço da página
    public List<Product> getProducts() {
        return driver.findElements(By.className("inventory_item"))
            .stream()
            .map(e -> new Product(e)) // Mapeia WebElement para um componente do produto
            .toList();
    }

    // Retorna um produto específico usando uma função booleana (predicado)
    // Este é o padrão de estratégia comportamental do GoF
    public Product getProduct(Predicate<Product> condition) {
        return getProducts()
            .stream()
            .filter(condition) // Filtra por nome de produto ou preço
            .findFirst()
            .orElseThrow();
    }
}

O objeto do componente Produto é usado dentro do objeto de página Produtos.

public abstract class BaseComponent {
    protected WebElement root;

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

// Objeto Componente da Página (Page Component Object)
public class Product extends BaseComponent {
    // O elemento raiz contém todo o componente
    public Product(WebElement root) {
        super(root); // inventory_item
    }

    public String getName() {
        // A localização de um elemento começa na raiz do componente
        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); // Higienização e formatação
    }

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

Agora, o teste dos produtos usaria o Page Objecto e o Page Component Obeject da seguinte maneira.

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

        // Passa uma expressão lambda (predicado) para filtrar a lista de produtos
        // O predicado ou "estratégia" é o comportamento passado como parâmetro
        var backpack = productsPage.getProduct(p -> p.getName().equals("Backpack")); // page component object
        var bikeLight = productsPage.getProduct(p -> p.getName().equals("Bike Light"));

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

A página e o componente são representados por seus próprios objetos. Ambos os objetos têm apenas métodos para os serviços que oferecem, o que corresponde à aplicação do mundo real na programação orientada a objetos.

Você pode até aninhar objetos de componentes dentro de outros objetos de componentes para páginas mais complexas. Se uma página na AUT tiver vários componentes, ou componentes comuns usados em todo o site (por exemplo, uma barra de navegação), então isso pode melhorar a manutenibilidade e reduzir a duplicação de código.

Outros Padrões de Projeto (Design Patterns) Usados em Testes

Existem outros padrões de projeto que também podem ser usados em testes. Discutir todos esses está além do escopo deste guia do usuário. Aqui, apenas queremos introduzir os conceitos para tornar o leitor ciente de algumas das coisas que podem ser feitas. Como foi mencionado anteriormente, muitos escreveram sobre este tópico e encorajamos o leitor a procurar blogs sobre esses tópicos.

Notas de Implementação

Page Objects podem ser pensados como se estivessem voltados para duas direções simultaneamente. Voltado para o desenvolvedor de um teste, eles representam os serviços oferecidos por uma página específica. Virado para longe do desenvolvedor, eles devem ser a única coisa que tem um conhecimento profundo da estrutura do HTML de uma página (ou parte de uma página). É mais simples pensar nos métodos de um Page Object como oferecendo os “serviços” que uma página oferece, em vez de expor os detalhes e a mecânica da página. Como exemplo, pense na caixa de entrada de qualquer sistema de email baseado na web. Entre os serviços que oferece estão a capacidade de compor um novo e-mail, escolher ler um único e-mail e listar as linhas de assunto dos e-mails na caixa de entrada. Como esses são implementados não deve importar para o teste.

Porque estamos encorajando o desenvolvedor de um teste a tentar pensar sobre os serviços com os quais estão interagindo em vez da implementação, os Page Objects raramente devem expor a instância subjacente do WebDriver. Para facilitar isso, os métodos no Page Object devem retornar outros Page Objects. Isso significa que podemos efetivamente modelar a jornada do usuário em nosso aplicativo. Também significa que se a maneira como as páginas se relacionam entre si mudar (como quando a página de login pede ao usuário para alterar sua senha na primeira vez que eles entram em um serviço quando antes não fazia isso), simplesmente mudando a assinatura do método apropriado fará com que os testes falhem em compilação. Colocando de outra forma; podemos dizer quais testes falhariam sem precisar executá-los quando mudamos a relação entre as páginas e refletimos isso nos PageObjects.

Uma consequência dessa abordagem é que pode ser necessário modelar (por exemplo) tanto um login bem-sucedido quanto um mal-sucedido; ou um clique poderia ter um resultado diferente dependendo do estado do aplicativo. Quando isso acontece, é comum ter vários métodos no PageObject:

public class LoginPage {
    public HomePage loginAs(String username, String password) {
        // ... mágica inteligente acontece aqui
    }
    
    public LoginPage loginAsExpectingError(String username, String password) {
        //  ... falha no login aqui, talvez porque o nome de usuário e/ou a senha estão incorretos
    }
    
    public String getErrorMessage() {
        // Para que possamos verificar se o erro correto é mostrado
    }
}

O código apresentado acima mostra um ponto importante: os testes, não os Page Objects, devem ser responsáveis por fazer asserções sobre o estado de uma página. Por exemplo:

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

Claro, como em toda diretriz, existem exceções, e uma que é comumente vista com Page Objects é verificar se o WebDriver está na página correta quando instanciamos o Page Object. Isso é feito no exemplo abaixo.

Finalmente, um Page Object não precisa representar uma página inteira. Pode representar uma seção que aparece com frequência dentro de um site ou página, como a navegação do site. O princípio essencial é que há apenas um lugar em sua suíte de testes com conhecimento da estrutura do HTML de uma determinada (parte de uma) página.

Resumo

  • Os métodos públicos representam os serviços que a página oferece
  • Tente não expor as entranhas da página
  • Geralmente não faça asserções
  • Métodos retornam outros PageObjects *Não precisa representar uma página inteira
  • Resultados diferentes para a mesma ação são modelados como métodos diferentes

Example

public class LoginPage {
    private final WebDriver driver;

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

        // Verifica se estamos na página correta.
        if (!"Login".equals(driver.getTitle())) {
            // Alternativamente, poderíamos navegar para a página de login, talvez fazendo logout primeiro
            throw new IllegalStateException("This is not the login page");
        }
    }

    // A página de login contém vários elementos HTML que serão representados como WebElements.
    // Os localizadores para esses elementos devem ser definidos apenas uma vez.
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // A página de login permite que o usuário digite seu nome de usuário no campo de nome de usuário
    public LoginPage typeUsername(String username) {
        // Este é o único lugar que "sabe" como entrar com um nome de usuário
        driver.findElement(usernameLocator).sendKeys(username);

        // Retorna o objeto de página atual, já que esta ação não navega para uma página representada por outro Page Object
        return this;	
    }
Este é o único lugar que "sabe" como entrar com uma senha
    // A página de login permite que o usuário digite sua senha no campo de senha
    public LoginPage typePassword(String password) {
        // Este é o único lugar que "sabe" como entrar com uma senha
        driver.findElement(passwordLocator).sendKeys(password);

        // Retorna o objeto de página atual, já que esta ação não navega para uma página representada por outro Page Object
        return this;	
    }

    // A página de login permite que o usuário envie o formulário de login
    public HomePage submitLogin() {
        // Este é o único lugar que envia o formulário de login e espera que o destino seja a página inicial.
        // Um método separado deve ser criado para a instância de clicar em login enquanto espera uma falha de login.
        driver.findElement(loginButtonLocator).submit();

        // Retorna um novo objeto de página representando o destino. Caso a página de login vá para algum outro lugar (por exemplo, um aviso legal),
        // então a alteração da assinatura do método para este método significará que todos os testes que dependem deste comportamento não serão compilados.
        return new HomePage(driver);	
    }

    // A página de login permite que o usuário envie o formulário de login sabendo que um nome de usuário inválido e/ou senha foram inseridos
    public LoginPage submitLoginExpectingFailure() {
        // Este é o único lugar que envia o formulário de login e espera que o destino seja a página de login devido à falha no login.
        driver.findElement(loginButtonLocator).submit();

        // Retorna um novo objeto de página representando o destino. Caso o usuário seja navegado para a página inicial depois de enviar um login com credenciais
        // que se espera falhar no login, o script falhará quando tentar instanciar o PageObject LoginPage.
        return new LoginPage(driver);	
    }

    // Conceitualmente, a página de login oferece ao usuário o serviço de ser capaz de "entrar"
    // no aplicativo usando um nome de usuário e senha. 
    public HomePage loginAs(String username, String password) {
        // Os métodos PageObject que inserem nome de usuário, senha e enviam login já foram definidos e não devem ser repetidos aqui.
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}

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.