Depois de concluirmos esta aula, nós:
- Saberemos como autenticar um usuário (
login) antes que um método de teste End-to-End seja executado.
- Saberemos escrever testes End-to-End expressivos que utilizam uma hierarquia de Page Objects.
- Entenderemos por que e como fazer o logout do usuário (
logUserOut) após a execução de cada método de teste.
1. Introdução ao Sistema Sob Teste (SUT): A Página de Visualização de Tarefa
Durante esta aula, escreveremos testes End-to-End para o método de controller que renderiza a página de visualização dos detalhes de uma única tarefa (view task page). Essa página atende aos seguintes requisitos de negócio:
- A página de visualização de tarefa é renderizada quando um usuário autenticado envia uma requisição
GET para o caminho /task/{taskId}.
- Esta página deve exibir o título (
#task-title) e a descrição (#task-description) da tarefa.
- Se a tarefa estiver atribuída a alguém, a página deve exibir o nome do responsável (
#assignee-name).
- A página deve exibir o horário de criação (
#creation-time) e o nome da pessoa que criou a tarefa exibida (#creator-name).
- A página deve exibir o horário da última modificação (
#modification-time) e o nome da pessoa que fez a última alteração na tarefa (#modifier-name).
- Se a tarefa estiver fechada (
CLOSED), a página deve exibir o container #closed-task-fields contendo o horário de fechamento (#closing-time) e o nome da pessoa que fechou a tarefa (#closer-name).
- A página deve exibir a seção de links de ação (
#task-action-links) contendo os links para excluir a tarefa (#delete-task-link) e para abrir a página de edição da tarefa (#edit-task-link / #update-task-link).
O template HTML/JSP da nossa página (view.jsp) 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
45
46
|
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<h1 id="task-title">${task.title}</h1>
<div class="panel panel-default">
<div class="panel-body">
<div id="task-description">${task.description}</div>
</div>
</div>
<div id="lifecycle-fields" class="container-fluid">
<!-- Renderizado apenas se task.assignee != null -->
<div class="row">
<div class="col-md-3">
Assignee: <span id="assignee-name">${task.assignee.name}</span>
</div>
</div>
<div class="row">
<div class="col-md-3">
Created by <span id="creator-name">${task.creator.name}</span>
at <span id="creation-time">${task.creationTime}</span>
</div>
<div class="col-md-3">
Modified by <span id="modifier-name">${task.modifier.name}</span>
at <span id="modification-time">${task.modificationTime}</span>
</div>
<!-- Renderizado apenas se task.status == 'CLOSED' -->
<div id="closed-task-fields" class="col-md-3">
Closed by <span id="closer-name">${task.closer.name}</span>
at <span id="closing-time">${task.modificationTime}</span>
</div>
</div>
<div id="task-action-links" class="row">
<div class="col-md-1">
<a id="delete-task-link" href="/task/${task.id}/delete">Delete</a>
</div>
<div class="col-md-1">
<a id="update-task-link" href="/task/${task.id}/update">Edit</a>
</div>
</div>
</div>
</body>
</html>
|
Vamos avançar e criar os cinco Page Objects necessários para modelar essa página e suas seções.
2. Criando os Page Objects Necessários
Antes de podermos escrever nossos testes End-to-End para a funcionalidade de visualização de tarefa, precisamos criar cinco Page Objects. Vamos examiná-los um por um:
1. O Page Object NavigationBar
A classe NavigationBar é um Page Object que representa a barra de navegação superior visível em todas as páginas da nossa aplicação web. Ela possui um método chamado logUserOut(), que encerra a sessão do usuário atual submetendo o formulário #logout-form e retorna uma nova instância de LoginPage. Nossos testes End-to-End invocarão esse método no @AfterEach após cada teste!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.testwithspring.intermediate.web
import com.testwithspring.intermediate.web.login.LoginPage
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
class NavigationBar(
private val browser: WebDriver
) {
fun logUserOut(): LoginPage {
if (browser.findElements(By.id("logout-form")).isEmpty()) {
throw RuntimeException("O botão de logout (#logout-form) não está visível na página.")
}
browser.findElement(By.id("logout-form")).submit()
return LoginPage(browser)
}
}
|
2. O Page Object TaskPage
A classe TaskPage é o Page Object principal que representa a página de visualização de uma tarefa (/task/{taskId}). Observe como ela aplica a 1ª e a 2ª Boas Práticas que aprendemos na Aula 03, delegando subseções da página para Page Objects especializados:
- Ela fornece dois métodos (
getTaskActions() e getTaskLifeCycleFields()) que retornam outros Page Objects, permitindo acessar as seções #task-action-links e #lifecycle-fields da página.
- Ela fornece dois métodos (
getTaskTitle() e getTaskDescription()) que retornam o título e a descrição da tarefa exibida como String.
- O método
open() abre a página de visualização da tarefa diretamente.
- O método
isOpen() retorna true se a página de visualização da tarefa estiver aberta 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
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package com.testwithspring.intermediate.web.task
import com.testwithspring.intermediate.WebDriverUrlBuilder
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
internal class TaskPage(
private val browser: WebDriver,
private val taskId: Long?
) {
private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/task/%d", taskId)
fun getTaskActions(): TaskActions {
return TaskActions(
browser = browser,
rootElement = browser.findElement(By.id("task-action-links")),
taskId = taskId
)
}
fun getTaskLifeCycleFields(): TaskLifeCycleFields {
return TaskLifeCycleFields(
rootElement = browser.findElement(By.id("lifecycle-fields"))
)
}
fun getTaskDescription(): String {
return browser.findElement(By.id("task-description")).text
}
fun getTaskTitle(): String {
return browser.findElement(By.id("task-title")).text
}
fun getPageUrl(): String = pageUrl
fun open(): TaskPage {
browser.get(pageUrl)
return TaskPage(browser, taskId)
}
fun isOpen(): Boolean {
return browser.currentUrl == pageUrl
}
}
|
3. O Page Object de Seção TaskLifeCycleFields
A classe TaskLifeCycleFields é um Page Object de fragmento de página que encapsula o elemento <div id="lifecycle-fields"> (rootElement) e nos permite consultar as informações de ciclo de vida e auditoria da tarefa exibida (criador, modificador, responsável e quem fechou a tarefa, além de verificar a visibilidade condicional dos campos):
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.task
import org.openqa.selenium.By
import org.openqa.selenium.WebElement
internal class TaskLifeCycleFields(
private val rootElement: WebElement
) {
fun getAssigneeName(): String {
return rootElement.findElement(By.id("assignee-name")).text
}
fun getCloserName(): String {
return rootElement.findElement(By.id("closer-name")).text
}
fun getCreatorName(): String {
return rootElement.findElement(By.id("creator-name")).text
}
fun getModifierName(): String {
return rootElement.findElement(By.id("modifier-name")).text
}
fun isAssigneeNameVisible(): Boolean {
return rootElement.findElements(By.id("assignee-name")).size == 1
}
fun areClosedTaskFieldsVisible(): Boolean {
return rootElement.findElements(By.id("closed-task-fields")).size == 1
}
}
|
Por que TaskLifeCycleFields busca tudo a partir de rootElement em vez de browser?
Porque rootElement já aponta para <div id="lifecycle-fields">. Ao invocar rootElement.findElement(...), restringimos a busca apenas aos elementos filhos daquela seção específica do DOM!
Além disso, observe que essa classe não expõe os timestamps (#creation-time, #modification-time) quando os scripts SQL utilizam now() dinâmico no banco, focando nos nomes determinísticos dos usuários de auditoria.
4. O Page Object de Seção TaskActions
A classe TaskActions é um Page Object que encapsula o container <div id="task-action-links"> e nos permite clicar nos dois links de ação da página de detalhes da tarefa:
- O método
deleteTask() clica no link #delete-task-link, aguarda com WebDriverWait até que o navegador seja redirecionado para a URL de TaskListPage e retorna a nova TaskListPage.
- O método
updateTask() clica no link #update-task-link, aguarda com WebDriverWait até que a página de edição UpdateTaskPage seja carregada e retorna a nova UpdateTaskPage.
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
|
package com.testwithspring.intermediate.web.task
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait
import java.time.Duration
internal class TaskActions(
private val browser: WebDriver,
private val rootElement: WebElement,
private val taskId: Long?
) {
fun deleteTask(): TaskListPage {
val deleteLink = rootElement.findElement(By.id("delete-task-link"))
deleteLink.click()
val targetPage = TaskListPage(browser)
val wait = WebDriverWait(browser, Duration.ofSeconds(3))
wait.until(ExpectedConditions.urlToBe(targetPage.getPageUrl()))
return targetPage
}
fun updateTask(): UpdateTaskPage {
val updateTaskLink = rootElement.findElement(By.id("update-task-link"))
updateTaskLink.click()
val targetPage = UpdateTaskPage(browser, taskId)
val wait = WebDriverWait(browser, Duration.ofSeconds(3))
wait.until(ExpectedConditions.urlToBe(targetPage.getPageUrl()))
return targetPage
}
}
|
5. O Page Object UpdateTaskPage
Por fim, a classe UpdateTaskPage é um Page Object que representa a página de atualização da tarefa (/task/{taskId}/update). Ela fornece os métodos getPageUrl() (usado pelo WebDriverWait dentro de TaskActions.updateTask()) e isOpen():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.testwithspring.intermediate.web.task
import com.testwithspring.intermediate.WebDriverUrlBuilder
import org.openqa.selenium.WebDriver
internal class UpdateTaskPage(
private val browser: WebDriver,
private val taskId: Long?
) {
private val pageUrl: String = WebDriverUrlBuilder.buildFromPath("/task/%d/update", taskId)
fun getPageUrl(): String = pageUrl
fun isOpen(): Boolean {
return browser.currentUrl == pageUrl
}
}
|
A seguir, veremos como utilizar esses cinco Page Objects para escrever nossos testes End-to-End de visualização de tarefa!
3. Escrevendo Testes End-to-End para a Visualização de Tarefa
Podemos construir nossa classe de teste End-to-End seguindo estes 5 passos:
Passo 1: Criar a Classe de Teste e Configurar o WebDriver
Primeiro, criamos a classe de teste RenderViewTaskPageTest e configuramos o navegador com @SeleniumTest:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.testwithspring.intermediate.web.task
import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.SeleniumWebDriver
import org.junit.jupiter.api.Tag
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderViewTaskPageTest {
@SeleniumWebDriver
lateinit var browser: WebDriver
}
|
Passo 2: Adicionar o Campo taskPage à Classe de Teste
Segundo, adicionamos uma propriedade private lateinit var taskPage: TaskPage à nossa classe de teste. Nossos métodos de teste utilizarão esse campo quando abrirem a página de visualização da tarefa:
1
2
3
4
5
6
7
8
9
|
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderViewTaskPageTest {
@SeleniumWebDriver
lateinit var browser: WebDriver
private lateinit var taskPage: TaskPage
}
|
Passo 3: Implementar o Método @BeforeEach e Autenticar o Usuário
Terceiro, adicionamos um método de setup anotado com @BeforeEach que executa duas ações:
- Autentica o usuário antes da execução do método de teste (
logUserIn()): Precisamos fazer isso porque nossa aplicação web exige autenticação e não permite que usuários anônimos acessem as tarefas armazenadas no banco de dados.
- Cria um novo objeto
TaskPage para o ID da tarefa conhecida (EndToEndTestTasks.WriteExampleApp.ID) e armazena a referência no campo taskPage.
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
|
import com.testwithspring.intermediate.EndToEndTestTasks
import com.testwithspring.intermediate.EndToEndTestUsers.JohnDoe
import com.testwithspring.intermediate.web.login.LoginPage
import org.junit.jupiter.api.BeforeEach
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderViewTaskPageTest {
@SeleniumWebDriver
lateinit var browser: WebDriver
private lateinit var taskPage: TaskPage
@BeforeEach
fun configureTestCases() {
logUserIn()
taskPage = TaskPage(
browser = browser,
taskId = EndToEndTestTasks.WriteExampleApp.ID
)
}
private fun logUserIn() {
val loginPage = LoginPage(browser).open()
loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD)
}
}
|
Observe que nossos testes End-to-End utilizam objetos de constantes (EndToEndTestUsers e EndToEndTestTasks) sincronizados com os dados inseridos pelos nossos scripts SQL:
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 EndToEndTestUsers {
object JohnDoe {
const val EMAIL_ADDRESS = "john.doe@gmail.com"
const val NAME = "John Doe"
const val PASSWORD = "user"
}
}
object EndToEndTestTasks {
object WriteExampleApp {
const val ID = 1L
const val TITLE = "Write example application"
val STATUS = TaskStatus.OPEN
object Creator {
const val ID = 1L
const val NAME = "John Doe"
}
}
}
|
Passo 4: Fazer Logout do Usuário no Método @AfterEach
Quarto, precisamos garantir que o usuário logado seja desconectado (logUserOut()) após a execução de cada método de teste. Esse passo é fundamental (especialmente quando a mesma sessão de navegador é reutilizada entre métodos de teste na mesma classe, como no SeleniumTestExecutionListener da Aula 03 do Tópico 06), pois garante que cada teste comece sem sessão ativa e autentique explicitamente o usuário correto!
1
2
3
4
|
@AfterEach
fun logUserOut() {
NavigationBar(browser).logUserOut()
}
|
Passo 5: Escrever os Quatro Métodos de Teste End-to-End
Quinto, escrevemos quatro testes End-to-End utilizando exclusivamente a DSL dos nossos Page Objects:
- Verificar se conseguimos abrir a página de visualização da tarefa (
shown.isOpen()).
- Garantir que a página de visualização exibe o título correto da tarefa (
shown.getTaskTitle()).
- Verificar se a subseção de ciclo de vida exibe o nome da pessoa que criou a tarefa (
lifeCycleFields.getCreatorName()).
- Garantir que clicar no link de edição abre a página de atualização da tarefa (
targetPage.isOpen()).
Veja o código completo da nossa classe RenderViewTaskPageTest:
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
package com.testwithspring.intermediate.web.task
import com.testwithspring.intermediate.EndToEndTestTasks
import com.testwithspring.intermediate.EndToEndTestUsers.JohnDoe
import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.SeleniumWebDriver
import com.testwithspring.intermediate.web.NavigationBar
import com.testwithspring.intermediate.web.login.LoginPage
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
@SeleniumTest(driver = ChromeDriver::class)
@Tag("endToEndTest")
class RenderViewTaskPageTest {
@SeleniumWebDriver
lateinit var browser: WebDriver
private lateinit var taskPage: TaskPage
@BeforeEach
fun configureTestCases() {
logUserIn()
taskPage = TaskPage(
browser = browser,
taskId = EndToEndTestTasks.WriteExampleApp.ID
)
}
private fun logUserIn() {
val loginPage = LoginPage(browser).open()
loginPage.login(JohnDoe.EMAIL_ADDRESS, JohnDoe.PASSWORD)
}
@Test
fun `deve abrir a pagina de visualizacao da tarefa`() {
val shown = taskPage.open()
assertThat(shown.isOpen()).isTrue()
}
@Test
fun `deve exibir o titulo da tarefa visualizada`() {
val shown = taskPage.open()
assertThat(shown.getTaskTitle())
.isEqualTo(EndToEndTestTasks.WriteExampleApp.TITLE)
}
@Test
fun `deve exibir o nome do criador da tarefa visualizada`() {
val shown = taskPage.open()
val lifeCycleFields = shown.getTaskLifeCycleFields()
assertThat(lifeCycleFields.getCreatorName())
.isEqualTo(EndToEndTestTasks.WriteExampleApp.Creator.NAME)
}
@Test
fun `deve permitir navegar para a pagina de atualizacao da tarefa`() {
val shown = taskPage.open()
val actions = shown.getTaskActions()
val targetPage = actions.updateTask()
assertThat(targetPage.isOpen()).isTrue()
}
@AfterEach
fun logUserOut() {
NavigationBar(browser).logUserOut()
}
}
|
Resumo
Esta aula nos ensinou três lições importantes:
- Se a nossa aplicação web não permite que usuários anônimos acessem a funcionalidade testada, devemos autenticar o usuário (
LoginPage(browser).open().login(...)) dentro do método @BeforeEach.
- Ao encapsularmos toda a API do
WebDriver dentro de Page Objects (TaskPage, TaskLifeCycleFields, TaskActions, UpdateTaskPage), nossos métodos @Test ficam extremamente curtos e expressam diretamente as regras de negócio da funcionalidade.
- Devemos fazer logout do usuário atual no método
@AfterEach (NavigationBar(browser).logUserOut()) para garantir que cada teste End-to-End subsequente seja executado com uma sessão limpa e com o usuário correto.