Testes E2E para SPAs JavaScript: Introdução e Desafios de Assincronicidade
Depois de concluirmos esta aula, nós:
- Saberemos a diferença crucial entre uma aplicação web tradicional (Server-Side Rendered) e uma aplicação web JavaScript (SPA) sob a ótica dos testes End-to-End.
- Entenderemos o que precisamos levar em consideração quando escrevemos testes End-to-End para aplicações web JavaScript.
- Saberemos fazer as alterações necessárias nos componentes e Page Objects utilizados para configurar nossos testes End-to-End (
WebDriverEnvironment,WebDriverUrlBuilder,SeleniumWait,LoginPageeNavigationBar).
1. A Diferença Entre uma Aplicação Web Tradicional e uma Aplicação Web JavaScript (SPA)
Naturalmente, uma aplicação web tradicional (renderizada no servidor via Thymeleaf/JSP) e uma aplicação web JavaScript (Single Page Application em React, Vue, Angular ou AngularJS) possuem várias diferenças arquiteturais. Entretanto, quando pensamos em testes End-to-End, a diferença mais crucial é:
- Uma aplicação web tradicional constrói o documento HTML inteiro no backend antes de retornar a resposta HTTP de volta para o navegador. Quando
browser.get()retorna, todos os elementos já estão no DOM. - A interface de usuário de uma aplicação web JavaScript (SPA) consiste em componentes de UI assíncronos que só se tornam visíveis quando seus dados chegam da API REST.
Em outras palavras, quando escrevemos testes End-to-End para uma aplicação web JavaScript, devemos seguir rigorosamente estas duas regras:
- Devemos garantir que é possível interagir com um componente de UI antes de tentarmos interagir com ele (por exemplo, aguardar que um botão ou input esteja visível e habilitado antes de clicar ou digitar).
- Devemos verificar que a informação desejada já está visível no DOM antes de executarmos asserções sobre ela.
Vamos avançar e descobrir quais alterações precisamos fazer antes de configurarmos nossos testes End-to-End para uma SPA.
2. Habilitando e Desabilitando o Modo HTML5 de Roteamento no Frontend
Quando nossa aplicação frontend utiliza roteamento no lado do cliente (como o HTML5 mode do AngularJS / History API vs Hashbang Router #/), precisamos ser capazes de habilitar ou desabilitar o modo HTML5 nos nossos utilitários de URL porque isso afeta diretamente os endereços URL utilizados pela nossa aplicação web:
- Se o modo HTML5 estiver desabilitado, nossa aplicação web utiliza endereços URL contendo um hash (
http://localhost:8080/#/user/login). - Se o modo HTML5 estiver habilitado, nossa aplicação utiliza endereços URL “normais” da History API que não contêm o caractere
#(http://localhost:8080/user/login).
Antes de escrevermos os testes End-to-End para a nossa SPA, precisamos construir nossos endereços URL de uma forma que suporte ambas as opções de configuração. Podemos fazer isso em dois passos:
Passo 1: Adicionar o Método isHtml5ModeEnabled() ao WebDriverEnvironment
Primeiro, adicionamos um novo método estático isHtml5ModeEnabled() ao objeto WebDriverEnvironment. Esse método lê o valor da propriedade de sistema webdriver.html5.mode.enabled e retorna true se o modo HTML5 estiver habilitado e false caso contrário. Se a propriedade de sistema não for encontrada, o método lança uma RuntimeException:
|
|
Passo 2: Atualizar o Método buildFromPath() no WebDriverUrlBuilder
Segundo, fazemos uma alteração no método buildFromPath() da classe WebDriverUrlBuilder: se o modo HTML5 estiver desabilitado (!WebDriverEnvironment.isHtml5ModeEnabled()), concatenamos a string "#/" logo após a barra final da URL base do servidor!
Depois de fazermos essa alteração, o código-fonte do WebDriverUrlBuilder fica assim:
|
|
Vamos descobrir agora como escrever testes End-to-End que aguardam até que uma condição específica do DOM seja atendida.
3. Aguardando Até Que uma Condição Específica Seja Atendida (SeleniumWait)
Como estamos escrevendo testes End-to-End para uma aplicação web JavaScript (SPA), não podemos assumir que todos os componentes de UI estão visíveis imediatamente após abrirmos uma rota. É por isso que a maioria dos nossos testes End-to-End precisa seguir este fluxo de 3 etapas:
- Executar uma operação que dispara alterações assíncronas no DOM (como abrir uma página ou clicar em um botão).
- Aguardar até que o DOM tenha sido atualizado.
- Executar as asserções necessárias.
Como muitos Page Objects precisarão de condições de espera semelhantes, é uma excelente ideia criar uma classe utilitária chamada SeleniumWait para eliminar código duplicado. Podemos criar essa classe seguindo estes três passos:
- Criar um
object(ou classefinalcom construtor privado) chamadoSeleniumWait. - Adicionar uma constante privada chamada
DEFAULT_TIMEOUT_IN_SECONDScom o valor5L(5 segundos). - Adicionar dois métodos utilitários iniciais:
waitUntilElementIsClickable(browser: WebDriver, elementLocator: By): Aguarda até que o elemento especificado esteja visível e habilitado para receber cliques (ExpectedConditions.elementToBeClickable).waitUntilElementIsVisible(browser: WebDriver, elementLocator: By): Aguarda até que o elemento especificado esteja presente no DOM e visível na tela (ExpectedConditions.visibilityOfElementLocated).
Depois de implementarmos esses métodos, o código-fonte de SeleniumWait fica assim:
|
|
Depois de criarmos esses métodos utilitários, precisamos atualizar os dois Page Objects responsáveis por autenticar o usuário antes de cada método de teste (LoginPage) e desconectar o usuário após cada método de teste (NavigationBar).
4. Autenticando o Usuário Antes do Teste na SPA (LoginPage)
A maior diferença entre o formulário de login da nossa aplicação web tradicional e o da nossa SPA em JavaScript é como a submissão acontece:
- Na aplicação web tradicional, clicar no botão de login dispara um
submitnativo de formulário HTML (POST /user/login). - Na nossa aplicação JavaScript (SPA), clicar no botão
#login-submit-buttoninvoca uma função JavaScript (submitLoginForm()) que envia uma requisição AJAX/Fetch assíncrona em background!
O template HTML que renderiza o formulário de login da nossa SPA possui a seguinte estrutura:
|
|
Nossos testes End-to-End utilizam a classe LoginPage para autenticar o usuário no @BeforeEach. Como precisamos garantir que o formulário de login já foi renderizado pelo JavaScript antes de tentarmos digitar o e-mail e a senha, fazemos quatro alterações na classe LoginPage:
- Escrevemos o método
waitUntilPageIsOpen(), que aguarda até que nosso teste possa clicar no botão#login-submit-button(SeleniumWait.waitUntilElementIsClickable(browser, By.id("login-submit-button"))). Isso garante que os testes não tentem digitar o e-mail e a senha antes que o componente de login esteja pronto no DOM. - Alteramos a implementação do método privado
submitLoginForm(): em vez de chamar.submit()no<form>, agora clicamos diretamente no botão#login-submit-button(browser.findElement(By.id("login-submit-button")).click()). - Renomeamos o método
open()paraopenAsAnonymousUser()e garantimos que ele chamewaitUntilPageIsOpen()antes de retornar o novo objetoLoginPage. (Renomeamos esse método porque, se abrirmos a página de login como um usuário já autenticado, o botão#login-submit-buttonnão será renderizado e usaremos outra condição de espera!). - Alteramos a implementação do método
login(): após clicar no botão de login, ele agora chamashown.waitUntilPageIsOpen()naTaskListPageantes de retorná-la!
Depois de aplicarmos essas quatro mudanças, o código-fonte da classe LoginPage fica assim:
|
|
(Veremos a implementação detalhada do método TaskListPage.waitUntilPageIsOpen() na próxima aula deste tópico).
A seguir, faremos as alterações necessárias no Page Object NavigationBar para desconectar o usuário ao final de cada teste.
5. Fazendo Logout do Usuário Após o Teste na SPA (NavigationBar)
Nossos testes End-to-End utilizam a classe NavigationBar quando querem desconectar o usuário no @AfterEach. Precisamos reescrever o método logUserOut() porque nossa nova interface JavaScript não possui mais um <form id="logout-form">: em vez disso, o usuário é desconectado quando clica no botão <button id="log-out-button">, que dispara uma chamada assíncrona de logout!
O template HTML que renderiza o botão de logout na SPA é o seguinte:
|
|
Podemos reescrever o método logUserOut() seguindo estes quatro passos:
- Lançar uma
RuntimeExceptioncaso o botão de logout (#log-out-button) não esteja visível na interface de usuário. - Clicar no botão
#log-out-button. - Aguardar até que a página de login esteja aberta e pronta (
shown.waitUntilPageIsOpen()). - Retornar o novo objeto
LoginPage.
Depois de fazermos essas alterações na classe NavigationBar, seu código-fonte fica assim:
|
|
Resumo
Esta aula nos ensinou três lições fundamentais sobre testes End-to-End em aplicações JavaScript (SPAs):
- Devemos garantir que é possível interagir com um componente de UI (
waitUntilElementIsClickable) antes de tentarmos clicar ou digitar nele. - Devemos verificar que a informação necessária já foi renderizada no DOM (
waitUntilElementIsVisible) antes de executarmos asserções sobre ela. - Devemos centralizar nossa lógica de espera explícita em uma classe utilitária (
SeleniumWait) e dentro dos métodoswaitUntilPageIsOpen()dos nossos Page Objects, eliminando código duplicado e mantendo as classes de teste limpas.