Gerenciando o Ciclo de Vida do Playwright no Kotest

Depois de concluirmos esta aula, nós:

  • Saberemos criar uma extensão customizada do Kotest que configura o Playwright antes da execução dos nossos cenários e libera os recursos reservados após a execução das especificações.
  • Compreenderemos como podemos configurar nossos testes End-to-End utilizando nossa extensão customizada.

Esta aula assume que:

  • Você sabe executar seus testes End-to-End utilizando o Gradle 9 (endToEndTest).
  • Você está familiarizado com o Microsoft Playwright para JVM/Kotlin.
  • Você sabe configurar seus testes End-to-End utilizando JUnit 6.
  • Você está familiarizado com o Kotest Framework.

Criando uma Extensão Customizada no Kotest

Antes de podermos escrever testes End-to-End com o Playwright no Kotest, temos que encontrar uma maneira limpa e reutilizável de:

  • Criar e configurar uma instância de Playwright e Browser antes que os cenários da especificação sejam executados (beforeSpec).
  • Criar um BrowserContext e uma Page limpos antes de cada cenário raiz (beforeContainer / beforeEach), permitindo que nossos Page Objects recebam a Page pronta para uso.
  • Fechar o BrowserContext após cada cenário (limpando cookies, localStorage e sessão de usuário autenticado) e encerrar o Browser e o processo do Playwright ao final da especificação (afterSpec) para liberar todos os recursos nativos.
  • Minimizar os impactos negativos de performance na nossa suíte de testes reutilizando o processo do navegador entre os cenários de uma mesma especificação.

O Kotest possui um modelo de extensões extremamente versátil e tipado. Se quisermos adicionar comportamento customizado ao ciclo de vida do Kotest, podemos implementar interfaces como SpecExtension, TestListener (ou BeforeSpecListener, AfterSpecListener, BeforeTestListener, AfterTestListener) ou criar uma extensão gerenciadora de recursos.

O Kotest suporta duas formas de registrar extensões:

  • Extensão global: Registrada em AbstractProjectConfig e carregada automaticamente para todas as especificações da suíte.
  • Extensão local explícita (ou guiada por anotação/registro na Spec): Registrada apenas nas classes de especificação End-to-End que realmente precisam de um navegador.

Durante esta aula, criaremos uma extensão local configurável (PlaywrightExtension) e uma anotação opcional @PlaywrightTest por três motivos:

  • Podemos configurar nossa extensão (por exemplo, escolhendo entre Chromium, Firefox ou WebKit e modo headless) diretamente ao registrá-la.
  • O registro explícito documenta claramente o comportamento adicionado ao ciclo de vida da especificação, evitando “mágica invisível” de extensões globais.
  • Carregamos o motor do Playwright apenas nas especificações End-to-End, mantendo os testes unitários e de integração instantâneos.

A seguir, descobriremos como podemos criar as anotações e tipos de configuração necessários.

Criando as Anotações e Opções de Configuração

Podemos criar a estrutura de configuração da nossa extensão seguindo estes passos:

Primeiro, criamos um enum BrowserTypeOption que define quais motores de navegador do Playwright podem ser escolhidos pela especificação (sendo CHROMIUM o padrão):

1
2
3
4
5
6
7
package com.testwithspring.master.e2e

enum class BrowserTypeOption {
    CHROMIUM,
    FIREFOX,
    WEBKIT
}

Segundo, se quisermos suportar configuração declarativa via anotação na classe de especificação (além do registro programático via extensions(...)), podemos criar a anotação @PlaywrightTest:

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

import io.kotest.core.extensions.ApplyExtension

@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@ApplyExtension(PlaywrightAnnotationExtension::class)
annotation class PlaywrightTest(
    val browser: BrowserTypeOption = BrowserTypeOption.CHROMIUM,
    val headless: Boolean = true
)

Nota sobre @ApplyExtension no Kotest 6:

O Kotest fornece a meta-anotação @ApplyExtension, que funciona de forma análoga ao @ExtensionAnnotation do Spock ou @ExtendWith do JUnit 6: qualquer Spec anotada com @PlaywrightTest ativa automaticamente a extensão associada!

Vamos avançar e descobrir como implementar o inicializador e o limpador do ciclo de vida do Playwright no Kotest.

Implementando os Listeners de Ciclo de Vida do Playwright

Precisamos gerenciar dois níveis de ciclo de vida na nossa extensão do Kotest:

  1. Nível da Especificação (beforeSpec e afterSpec): Inicia o processo Playwright e lança a instância do Browser uma única vez por classe de especificação, porque iniciar um binário de navegador do zero é uma operação relativamente custosa. Ao final de todos os cenários da classe (afterSpec), fecha o Browser e encerra o Playwright.
  2. Nível do Cenário Raiz (beforeContainer / afterContainer ou beforeTest / afterTest): Cria um BrowserContext isolado (como uma janela anônima novinha em folha) e uma Page antes de cada teste raiz, e fecha esse BrowserContext ao terminar o cenário. Isso garante que um usuário logado em um cenário já esteja deslogado no próximo!

Criando e Encerrando o Playwright, Browser e Page

Vamos implementar a classe PlaywrightExtension implementando BeforeSpecListener, AfterSpecListener, BeforeTestListener e AfterTestListener do Kotest:

 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
package com.testwithspring.master.e2e

import com.microsoft.playwright.Browser
import com.microsoft.playwright.BrowserContext
import com.microsoft.playwright.BrowserType
import com.microsoft.playwright.Page
import com.microsoft.playwright.Playwright
import io.kotest.core.listeners.AfterSpecListener
import io.kotest.core.listeners.AfterTestListener
import io.kotest.core.listeners.BeforeSpecListener
import io.kotest.core.listeners.BeforeTestListener
import io.kotest.core.spec.Spec
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult

class PlaywrightExtension(
    private val browserOption: BrowserTypeOption = BrowserTypeOption.CHROMIUM,
    private val headless: Boolean = true
) : BeforeSpecListener, AfterSpecListener, BeforeTestListener, AfterTestListener {

    private var playwright: Playwright? = null
    private var browser: Browser? = null
    private var currentContext: BrowserContext? = null
    private var currentPage: Page? = null

    val page: Page
        get() = currentPage
            ?: error("A Page do Playwright ainda não foi inicializada. Acesse 'page' apenas dentro dos blocos Given/When/Then ou beforeEach.")

    val browserContext: BrowserContext
        get() = currentContext
            ?: error("O BrowserContext do Playwright ainda não foi inicializado.")

    override suspend fun beforeSpec(spec: Spec) {
        val pw = Playwright.create()
        playwright = pw

        val launchOptions = BrowserType.LaunchOptions().setHeadless(headless)
        browser = when (browserOption) {
            BrowserTypeOption.CHROMIUM -> pw.chromium().launch(launchOptions)
            BrowserTypeOption.FIREFOX -> pw.firefox().launch(launchOptions)
            BrowserTypeOption.WEBKIT -> pw.webkit().launch(launchOptions)
        }
    }

    override suspend fun beforeTest(testCase: TestCase) {
        if (testCase.descriptor.isRootTest()) {
            val activeBrowser = browser
                ?: error("O Browser do Playwright não foi inicializado no beforeSpec.")
            val ctx = activeBrowser.newContext()
            currentContext = ctx
            currentPage = ctx.newPage()
        }
    }

    override suspend fun afterTest(testCase: TestCase, result: TestResult) {
        if (testCase.descriptor.isRootTest()) {
            currentPage?.close()
            currentPage = null
            currentContext?.close()
            currentContext = null
        }
    }

    override suspend fun afterSpec(spec: Spec) {
        runCatching { browser?.close() }
        browser = null
        runCatching { playwright?.close() }
        playwright = null
    }
}

Vamos analisar passo a passo o que a nossa PlaywrightExtension faz:

  1. No beforeSpec(spec: Spec), ela invoca Playwright.create() e lança o navegador configurado (CHROMIUM, FIREFOX ou WEBKIT) no modo headless desejado.
  2. No beforeTest(testCase: TestCase), quando o testCase é um cenário raiz (testCase.descriptor.isRootTest()), ela abre um BrowserContext isolado e uma nova Page. Dessa forma, todos os blocos internos When e Then daquele Given compartilham a mesma aba do navegador!
  3. No afterTest(testCase: TestCase, result: TestResult), ao concluir o cenário raiz, ela fecha a Page e o BrowserContext, descartando instantaneamente todos os cookies de autenticação e estado da sessão.
  4. No afterSpec(spec: Spec), ela fecha o Browser e encerra o processo do Playwright, garantindo zero vazamento de processos Chromium no sistema operacional.

Usando Nossa Extensão Customizada do Kotest

Podemos usar nossa PlaywrightExtension nas especificações End-to-End seguindo três passos simples:

  1. Criar uma nova classe de especificação e anotá-la com @EndToEndTest para que seus cenários sejam executados quando rodarmos a task endToEndTest.
  2. Instanciar e registrar a PlaywrightExtension usando install(...) ou extensions(...) dentro da especificação.
  3. Acessar playwright.page dentro dos nossos cenários ou Page Objects.

O código-fonte da nossa classe de especificação 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
package com.testwithspring.master.e2e

import com.testwithspring.master.EndToEndTest
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe

@EndToEndTest
class ExampleEndToEndSpec : BehaviorSpec({

    val playwright = PlaywrightExtension(
        browserOption = BrowserTypeOption.CHROMIUM,
        headless = true
    )
    extensions(playwright)

    Given("que a aplicação web está em execução") {
        When("o usuário navega para a página inicial") {
            playwright.page.navigate("http://localhost:8080/")

            Then("o título da página deve ser exibido corretamente") {
                playwright.page.title() shouldBe "TaskTracker"
            }
        }
    }
})

Agora sabemos criar uma extensão customizada do Kotest que configura e gerencia o ciclo de vida do Playwright e utilizá-la ao escrever testes End-to-End. Na próxima aula, integraremos essa extensão com o servidor embarcado do Spring Boot 4 e o Testcontainers! Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou cinco coisas:

  • Podemos adicionar comportamento customizado ao ciclo de vida do Kotest implementando BeforeSpecListener, AfterSpecListener, BeforeTestListener e AfterTestListener.
  • Extensões locais registradas na especificação documentam explicitamente os recursos que participam do ciclo de vida do teste.
  • Devemos inicializar o Playwright e o Browser uma única vez por classe de especificação (beforeSpec) para maximizar a velocidade da suíte.
  • Devemos criar um novo BrowserContext e Page para cada cenário raiz (isRootTest()) e fechá-los ao final do cenário para garantir isolamento total de sessão e cookies.
  • Devemos encerrar o Browser e o Playwright no afterSpec para liberar todos os processos nativos ao término da especificação.