Criando Nosso Primeiro Page Object

Depois de concluirmos esta aula, nós:

  • Saberemos como criar Page Objects que não dependem de configurações ou URLs fixas (hardcoded) de um ambiente específico.
  • Saberemos criar Page Objects capazes de interagir com uma página HTML (preencher formulários e navegar).
  • Saberemos criar Page Objects capazes de extrair informações de uma página HTML.

1. Passando Configurações Específicas de Ambiente para Nossos Page Objects

Antes de escrevermos nosso primeiro Page Object, precisamos criar duas classes utilitárias que nos ajudam a construir Page Objects totalmente independentes de URLs fixas de ambiente. Podemos criar essas duas classes seguindo estes passos:

Passo 1: Criando a Classe WebDriverEnvironment

Primeiro, criamos um object (ou classe utilitária com métodos estáticos) chamado WebDriverEnvironment. Essa classe fornece o método getBaseUrl(), que nos permite acessar a configuração de URL base passada para nossos testes End-to-End via propriedades de sistema da JVM (System.getProperty("webdriver.base.url")) ou com fallback padrão para http://localhost:8080.

Implementamos o método getBaseUrl() seguindo estes três passos:

  1. Obter o valor da propriedade de sistema webdriver.base.url.
  2. Se nenhuma URL base for encontrada, lançar uma RuntimeException explicativa.
  3. Retornar a URL base encontrada.

Depois de implementarmos o método getBaseUrl(), o código-fonte de WebDriverEnvironment fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.testwithspring.intermediate

object WebDriverEnvironment {

    @JvmStatic
    fun getBaseUrl(): String {
        return System.getProperty("webdriver.base.url")
            ?: throw RuntimeException(
                "Nenhuma URL base encontrada! Configure a system property 'webdriver.base.url' no build."
            )
    }
}

Passo 2: Criando a Classe WebDriverUrlBuilder

Segundo, criamos um object chamado WebDriverUrlBuilder. Essa classe fornece um método fábrica estático (buildFromPath) que nos ajuda a substituir URLs fixas (hardcoded) por URLs construídas dinamicamente a partir da URL base do ambiente. Em outras palavras, essa classe permite que nossos testes End-to-End sejam executados em diferentes portas ou ambientes (local, Docker, staging) sem alterar uma única linha dos Page Objects!

Implementamos o método buildFromPath() seguindo estes 7 passos:

  1. Adicionar o método buildFromPath(path: String, vararg params: Any?) ao WebDriverUrlBuilder. Esse método recebe o template do caminho (path) e os parâmetros referenciados pelos especificadores de formatação (%d, %s, etc.) presentes na string do caminho, retornando o endereço URL completo.
  2. Se o path for vazio, lançar uma IllegalArgumentException.
  3. Criar o caminho real formatado utilizando String.format(path, *params).
  4. Obter a URL base invocando WebDriverEnvironment.getBaseUrl().
  5. Se a URL base não terminar com o caractere '/', concatenar '/' ao final da URL base.
  6. Se o caminho formatado começar com o caractere '/', remover esse primeiro caractere '/' para evitar barras duplas (//).
  7. Concatenar o caminho à URL base e retornar o endereço URL resultante.

Depois de escrevermos o método buildFromPath(), o código-fonte de WebDriverUrlBuilder fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.testwithspring.intermediate

object WebDriverUrlBuilder {

    @JvmStatic
    fun buildFromPath(path: String?, vararg params: Any?): String {
        if (path.isNullOrEmpty()) {
            throw IllegalArgumentException("O caminho (path) deve ser informado.")
        }

        var formattedPath = String.format(path, *params)

        var baseUrl = WebDriverEnvironment.getBaseUrl()
        if (!baseUrl.endsWith("/")) {
            baseUrl += "/"
        }

        if (formattedPath.startsWith("/")) {
            formattedPath = formattedPath.replaceFirst("/", "")
        }

        return baseUrl + formattedPath
    }
}

Vamos avançar agora e examinar a página de login da nossa aplicação web.


2. Introdução ao Sistema Sob Teste (SUT): A Página de Login

Durante esta aula, criaremos um Page Object que representa a página de login da nossa aplicação web. Essa página atende aos seguintes requisitos:

  • A página de login é renderizada quando um usuário envia uma requisição GET para o caminho /user/login.
  • O formulário de login é exibido apenas para usuários anônimos. Se um usuário já autenticado abrir a página de login, ele verá uma mensagem explicando que já está logado (#authenticated-user-error).
  • Um usuário pode fazer login informando seu endereço de e-mail (#email-address) e senha (#password).
  • Se o login falhar, o usuário é redirecionado de volta para a página de login (/user/login?error=bad_credentials), que deve exibir uma mensagem de erro (#login-error-alert) explicando que a autenticação falhou.

O código HTML/JSP da página de login (login.jsp / template Thymeleaf equivalente) possui a seguinte estrutura:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
<h1>Login</h1>
<div class="panel panel-default">
    <div class="panel-body">
        <!-- Exibido apenas para usuários anônimos (isAnonymous()) -->
        <div id="login-error-alert" class="alert alert-danger alert-dismissible">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            Login failed. Incorrect email address or password.
        </div>

        <form id="login-form" action="/user/login" method="POST">
            <input type="hidden" name="_csrf" value="..."/>
            <div class="form-group">
                <label for="email-address">Email address</label>
                <input id="email-address"
                       class="form-control"
                       name="username"
                       type="email"
                       placeholder="Enter email address">
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input id="password"
                       class="form-control"
                       name="password"
                       type="password"
                       placeholder="Enter password">
            </div>
            <button type="submit" class="btn btn-default">Login</button>
        </form>

        <!-- Exibido apenas para usuários já autenticados (isAuthenticated()) -->
        <div id="authenticated-user-error">
            You are already logged in.
        </div>
    </div>
</div>
</body>
</html>

A seguir, construiremos passo a passo o Page Object que representa essa página de login.


3. Criando o Page Object LoginPage

Podemos criar nosso Page Object seguindo estes 6 passos:

Passo 1: Criar a Classe LoginPage e Declarar seus Campos

Primeiro, criamos uma classe pública e final (class em Kotlin já é public e final por padrão) chamada LoginPage e adicionamos dois campos privados a ela:

  1. O campo browser contém a referência para o objeto WebDriver utilizado.
  2. O campo pageUrl contém a URL completa da página de login.
1
2
3
4
5
6
7
8
9
package com.testwithspring.intermediate.web.login

import org.openqa.selenium.WebDriver

class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String
}

Nota sobre Visibilidade: A classe LoginPage deve ser public porque será utilizada por outras classes de teste End-to-End (como os testes de tarefas) que não estão no mesmo pacote com.testwithspring.intermediate.web.login para autenticar o usuário antes dos testes! Se um Page Object for usado apenas por classes do mesmo pacote/módulo, você pode restringir a visibilidade de seus métodos auxiliares de inspeção com internal.

Passo 2: Inicializar a URL do Ambiente no Construtor

Segundo, inicializamos o campo pageUrl invocando WebDriverUrlBuilder.buildFromPath("/user/login"):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.testwithspring.intermediate.web.login

import com.testwithspring.intermediate.WebDriverUrlBuilder
import org.openqa.selenium.WebDriver

class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/user/login")
}

Passo 3: Implementar o Método open()

Terceiro, implementamos um método open() que abre a página de login no navegador e retorna um novo objeto LoginPage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/user/login")

    fun open(): LoginPage {
        browser.get(pageUrl)
        return LoginPage(browser)
    }
}

Regra de Ouro: Todo Page Object que representa uma página HTML inteira deve ter um método open() que abre a página em questão diretamente via URL. Como esse método nos permite navegar direto para a página testada sem precisar clicar por toda a aplicação desde a home page, podemos escrever testes End-to-End muito mais rápidos e com menos código de setup!

Passo 4: Implementar o Método isOpen()

Quarto, implementamos um método isOpen() que verifica se a página de login está atualmente aberta no navegador. Esse método retorna true se a URL atual do navegador for igual a pageUrl, e false caso contrário:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/user/login")

    fun open(): LoginPage {
        browser.get(pageUrl)
        return LoginPage(browser)
    }

    internal fun isOpen(): Boolean {
        return browser.currentUrl == pageUrl
    }
}

Por que criar isOpen() em todo Page Object de página inteira? Verificar se uma determinada página foi aberta após uma ação é uma asserção extremamente comum nos testes End-to-End (assertThat(loginPage.isOpen()).isTrue()). Ter o método isOpen() no Page Object mantém o objeto WebDriver e as URLs encapsulados dentro do Page Object.

Passo 5: Implementar os Métodos de Consulta de Informações da Página

Quinto, implementamos três métodos que buscam informações no DOM da página de login. Esses métodos são utilizados pelos testes End-to-End que verificam se a página de login foi renderizada corretamente:

  1. getEmailAddress(): Retorna o endereço de e-mail atualmente preenchido no input #email-address.
  2. getPassword(): Retorna a senha atualmente preenchida no input #password.
  3. isLoginFormVisible(): Retorna true se o formulário #login-form estiver visível no DOM (usando !browser.findElements(By.id("login-form")).isEmpty()) e false caso contrário.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.testwithspring.intermediate.web.login

import com.testwithspring.intermediate.WebDriverUrlBuilder
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver

class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/user/login")

    fun open(): LoginPage {
        browser.get(pageUrl)
        return LoginPage(browser)
    }

    internal fun isOpen(): Boolean {
        return browser.currentUrl == pageUrl
    }

    internal fun getEmailAddress(): String? {
        return browser.findElement(By.id("email-address")).getAttribute("value")
    }

    internal fun getPassword(): String? {
        return browser.findElement(By.id("password")).getAttribute("value")
    }

    internal fun isLoginFormVisible(): Boolean {
        return browser.findElements(By.id("login-form")).isNotEmpty()
    }
}

Passo 6: Implementar o Método login() e os Métodos Utilitários de Formulário

Sexto, implementamos um método público login(emailAddress: String, password: String): TaskListPage que autentica um usuário utilizando seu e-mail e senha. Esse método é utilizado para autenticar o usuário antes da execução dos nossos testes End-to-End e assume que o login será bem-sucedido.

Implementamos esse método em três subpassos:

  1. Escrever dois métodos privados utilitários (typeEmailAddress e typePassword) que digitam o e-mail e a senha nos campos do formulário de login.
  2. Implementar um método privado utilitário (submitLoginForm) que submete o formulário #login-form.
  3. Adicionar o método público login(emailAddress: String, password: String) à classe LoginPage, invocando os três métodos utilitários e retornando um novo objeto TaskListPage(browser).

Depois de implementarmos o método login(), o código-fonte completo da classe LoginPage fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.testwithspring.intermediate.web.login

import com.testwithspring.intermediate.WebDriverUrlBuilder
import com.testwithspring.intermediate.web.task.TaskListPage
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver

class LoginPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/user/login")

    fun open(): LoginPage {
        browser.get(pageUrl)
        return LoginPage(browser)
    }

    internal fun isOpen(): Boolean {
        return browser.currentUrl == pageUrl
    }

    internal fun getEmailAddress(): String? {
        return browser.findElement(By.id("email-address")).getAttribute("value")
    }

    internal fun getPassword(): String? {
        return browser.findElement(By.id("password")).getAttribute("value")
    }

    internal fun isLoginFormVisible(): Boolean {
        return browser.findElements(By.id("login-form")).isNotEmpty()
    }

    fun login(emailAddress: String, password: String): TaskListPage {
        typeEmailAddress(emailAddress)
        typePassword(password)
        submitLoginForm()

        return TaskListPage(browser)
    }

    private fun typeEmailAddress(emailAddress: String) {
        browser.findElement(By.id("email-address")).sendKeys(emailAddress)
    }

    private fun typePassword(password: String) {
        browser.findElement(By.id("password")).sendKeys(password)
    }

    private fun submitLoginForm() {
        browser.findElement(By.id("login-form")).submit()
    }
}

Por que extraímos os métodos privados typeEmailAddress, typePassword e submitLoginForm? Porque eles tornam o método login() muito mais legível e podem ser reutilizados caso adicionemos um segundo método loginAndExpectFailure(emailAddress: String, password: String): LoginPage para testar tentativas de login com credenciais inválidas!

O método login() retorna um novo objeto TaskListPage porque nossa aplicação web redireciona o usuário para a página de listagem de tarefas (/) quando o login é bem-sucedido. A estrutura inicial da classe TaskListPage é a seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.intermediate.web.task

import com.testwithspring.intermediate.WebDriverUrlBuilder
import org.openqa.selenium.WebDriver

class TaskListPage(
    private val browser: WebDriver
) {
    private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/")

    fun isOpen(): Boolean {
        return browser.currentUrl == pageUrl
    }
}

Resumo

Esta aula nos ensinou quatro pontos essenciais:

  • Devemos passar as propriedades de configuração específicas do ambiente (como webdriver.base.url) para os nossos testes End-to-End por meio de propriedades de sistema (WebDriverEnvironment).
  • Devemos construir todas as URLs dos Page Objects concatenando os caminhos relativos à URL base do ambiente com WebDriverUrlBuilder.buildFromPath(...).
  • Se um Page Object representa uma página HTML inteira, ele deve possuir um método open() que navega diretamente para a página em questão.
  • Se um Page Object representa uma página HTML inteira, ele deve possuir um método isOpen() que verifica se a URL atual do navegador corresponde à URL daquela página.