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.
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.
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&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.