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

Return to the regular view of this page.

Diretrizes e recomendações

Some guidelines and recommendations on testing from the Selenium project.

Page being translated from English to Portuguese. Do you speak Portuguese? Help us to translate it by sending us pull requests!

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

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

3 - Modelos de objetos de página

Objeto de página é um padrão de design que se tornou popular na automação de teste para melhorar a manutenção de teste e reduzir a duplicação de código. Um objeto de página é uma classe orientada a objetos que serve como uma interface para uma página de seu AUT. Os testes então usam os métodos desta classe de objeto de página sempre que precisam interagir com a interface do usuário dessa página. O benefício é que, se a IU mudar para a página, os próprios testes não precisam ser alterados, apenas o código dentro do o objeto da página precisa ser alterado. Posteriormente, todas as alterações para oferecer suporte a essa nova IU estão localizados em um só lugar.

O padrão de design do objeto de página oferece as seguintes vantagens:

  • Há uma separação clara entre o código de teste e o código específico da página, como localizadores (ou seu uso se você estiver usando um mapa de interface do usuário) e layout.
  • Existe um único repositório para os serviços ou operações oferecidos pela página em vez de ter esses serviços espalhados pelos testes.

Em ambos os casos, isso permite qualquer modificação necessária devido a mudanças na IU ser feito em um só lugar. Informações úteis sobre esta técnica podem ser encontradas em vários blogs, já que esse ‘padrão de design de teste’ está se tornando amplamente usado. Nós incentivamos o leitor que deseja saber mais a pesquisar blogs na internet nesse assunto. Muitos escreveram sobre este padrão de design e podem fornecer dicas úteis que vão além do escopo deste guia do usuário. Para começar, no entanto, vamos ilustrar objetos de página com um exemplo simples.

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

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

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

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

Há dois problemas com esta abordagem.

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

Aplicando as técnicas de objeto de página, este exemplo poderia ser reescrito assim no exemplo a seguir de um objeto de página para uma página de Sign-in.

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

  /**
    * Login como um usuário válido
    *
    * @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);
  }
}

e o objeto de página de uma página inicial pode ter a seguinte aparência.

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

/**
 * Page Object encapsula a 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() {
    // Encapsulamento da página para gerenciar a funcionalidade do perfil
    return new HomePage(driver);
  }
  /* Mais métodos fornecendo o serviços representados pela Home Page
  do usuário logado. Esses métodos por sua vez podem retornar mais Page Objects
  por exemplo clicar no botão Compor Email poderia retornar um objeto ComposeMail */
}

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

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

}

Há muita flexibilidade em como os objetos de página podem ser projetados, mas existem algumas regras básicas para obter a manutenção desejada de seu código de teste.

Os próprios objetos de página nunca devem fazer verificações ou afirmações. Isto é parte do seu teste e deve estar sempre dentro do código do teste, nunca em um objeto de página. O objeto da página conterá a representação da página, e o 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 que é para verificar se a página e, possivelmente, elementos críticos em a página, foram carregados corretamente. Esta verificação deve ser feita enquanto instanciar o objeto de página. Nos exemplos acima, ambos SignInPage e os construtores da HomePage verificam se a página esperada está disponível e pronta para solicitações do teste.

Um objeto de página não precisa necessariamente representar todas as partes da página em si. Os mesmos princípios usados para objetos de página podem ser usados para criar “Objetos de Componente de Página” que representam pedaços discretos da página e podem ser incluídos em objetos de página. Esses objetos de componentes podem fornecer referências aos elementos dentro desses blocos discretos, e métodos para utilizar a funcionalidade fornecida por eles. Você também pode aninhar objetos de componentes dentro de outros objetos de componentes para páginas mais complexas. Se uma página na aplicação tem vários componentes, ou componentes usados em todo o site (por exemplo, uma barra de navegação), então pode melhorar a manutenção e reduzir a duplicação de código.

Existem outros padrões de design que também podem ser usados em testes. Alguns usam um Page Factory para instanciar seus objetos de página. Discutir tudo isso é além do escopo deste guia do usuário. Aqui, queremos apenas apresentar o conceitos para tornar o leitor ciente de algumas coisas que podem ser feitas. Como foi mencionado anteriormente, muitos escreveram sobre este tópico e nós encorajamos o leitor para pesquisar blogs sobre esses tópicos.

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

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

6 - Simulação de serviços externos

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

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

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

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

10 - 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();
gsp.withFluent().setSearchString().clickSearchButton();

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

public class GoogleSearchPage extends LoadableComponent<GoogleSearchPage> {
  private final WebDriver driver;
  private GSPFluentInterface gspfi;

  public class GSPFluentInterface {
    private GoogleSearchPage gsp;

    public GSPFluentInterface(GoogleSearchPage googleSearchPage) {
        gsp = googleSearchPage;
    }

    public GSPFluentInterface clickSearchButton() {
        gsp.searchButton.click();
        return this;
    }

    public GSPFluentInterface setSearchString( String sstr ) {
        clearAndType( gsp.searchField, sstr );
        return this;
    }
  }

  @FindBy(id = "gbqfq") private WebElement searchField;
  @FindBy(id = "gbqfb") private WebElement searchButton;
  public GoogleSearchPage(WebDriver driver) {
    gspfi = new GSPFluentInterface( this );
    this.get(); // Se load() falhar, chama isLoaded() até que a página termine de carregar
    PageFactory.initElements(driver, this); // Inicializa WebElements na página
  }

  public GSPFluentInterface withFluent() {
    return gspfi;
  }

  public void clickSearchButton() {
    searchButton.click();
  }

  public void setSearchString( String sstr ) {
    clearAndType( searchField, sstr );
  }

  @Override
  protected void isLoaded() throws Error {
    Assert.assertTrue("Google search page is not yet loaded.", isSearchFieldVisible() );
  }

  @Override
  protected void load() {
    if ( isSFieldPresent ) {
      Wait<WebDriver> wait = new WebDriverWait( driver, Duration.ofSeconds(3) );
      wait.until( visibilityOfElementLocated( By.id("gbqfq") ) ).click();
    }
  }
}

11 - 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. Para Firefox, inicie um WebDriver com seu perfil conhecido.

FirefoxProfile profile = new FirefoxProfile(new File("pathToFirefoxProfile"));
WebDriver driver = new FirefoxDriver(profile);