Nossa Stack Tecnológica & Processo de Testes

O projeto Testando com Spring é uma recriação profunda, moderna e pedagógica em língua portuguesa do aclamado curso Test With Spring (© Koodikupla Oy). O objetivo fundamental desta iniciativa é libertar o desenvolvedor de práticas e ferramentas defasadas (como Java 8 legados, Spock com Groovy não tipado, Selenium flutuante e bancos em memória H2 artificiais), apresentando o estado da arte do ecossistema JVM moderno.

Nesta aula detalhamos a stack das aplicações sob teste ensinada ao longo das aulas e apresentamos um guia prático de engenharia passo a passo demonstrando como construímos a suíte de testes automatizados do próprio site utilizando a biblioteca padrão unittest do Python integrada ao Playwright e ao servidor Hugo via Devbox.


1. Nossa Stack Tecnológica

Todo o código, arquitetura e convenções abordados no curso utilizam estritamente versões de ponta e recursos contemporâneos parametrizados de forma centralizada:

Tecnologia Versão de Referência Gerenciamento / Comando Propósito na Arquitetura
Java JDK 21 ou 25 (LTS) sdk install java 21.0.6-tem Runtime e compilador moderno da plataforma JVM gerenciado via SDKMAN.
Kotlin 2.4.21+ sdk install kotlin 2.4.21 Linguagem concisa com suporte nativo a null safety, imutabilidade e corrotinas.
Spring Boot 4.1.2+ curl https://start.spring.io/starter.zip ... Framework base para microsserviços, injeção de dependências e APIs web.
Gradle 9.8.2+ (Kotlin DSL) sdk install gradle 9.8.2 Motor de compilação modular, rápido e configurado via build.gradle.kts.
JUnit Jupiter 6.0.3+ testImplementation(libs.junit.jupiter) Motor moderno da arquitetura JUnit Platform para testes unitários na JVM.
Kotest 6.2.6+ testImplementation(libs.kotest.runner.junit5) Framework BDD idiomático em Kotlin com DescribeSpec e matchers expressivos.
AssertJ Core 3.27.7+ testImplementation(libs.assertj.core) Biblioteca de asserções fluentes com ricas mensagens diagnósticas.
MockK 1.14.0+ testImplementation(libs.mockk) Biblioteca de mocks e stubs de primeira classe para Kotlin.
Testcontainers 2.0.5+ testImplementation(libs.testcontainers.postgresql) Containers reais efêmeros (PostgreSQL) para testes de integração confiáveis.
Playwright 1.50.0+ testImplementation(libs.playwright) Automação e crawling End-to-End resiliente para Chromium, Firefox e WebKit.
Hugo 0.169.0 devbox run -- hugo Gerador estático do portal educacional e documentação de aula.

#### Gerenciamento de Versões do Java e Gradle com SDKMAN Recomendamos utilizar o [SDKMAN!](https://sdkman.io/) para instalar e alternar versões do JDK e do Gradle de forma isolada, reprodutível e sem conflitos no sistema operacional: ```bash # 1. Instale o SDKMAN (se ainda não tiver instalado) curl -s "https://get.sdkman.io" | bash source "$HOME/.sdkman/bin/sdkman-init.sh" # 2. Instale o Java JDK 21 LTS (Eclipse Temurin) sdk install java 21.0.6-tem # 3. (Opcional) Instale o Gradle e o Kotlin CLI sdk install gradle 9.8.2 sdk install kotlin 2.4.21 # 4. Defina o Java 21 como a versão ativa no seu terminal sdk use java 21.0.6-tem # 5. Valide as versões instaladas java -version gradle -version ``` > [!TIP] > **Configuração Automática via `.sdkmanrc`**: > Você pode criar um arquivo chamado `.sdkmanrc` na raiz do seu projeto com o seguinte conteúdo: > ```properties > java=21.0.6-tem > gradle=9.8.2 > ``` > Depois, basta rodar `sdk env` na pasta do projeto (ou habilitar `sdkman_auto_env=true` no arquivo `~/.sdkman/etc/config`) para que o SDKMAN selecione automaticamente o Java e Gradle corretos sempre que você entrar no diretório!

2. A Pirâmide e Filosofia de Testes

Ao longo das lições, seguimos uma disciplina pragmática de engenharia de software:

flowchart TD E2E["Testes End-to-End (E2E)
Playwright + Testcontainers
Validação do fluxo real de ponta a ponta"] IT["Testes de Integração
Spring Boot + PostgreSQL Real (@ServiceConnection)
Persistência, Repositórios, Segurança e Controllers"] UT["Testes Unitários Rápidos
JUnit Jupiter / Kotest + MockK + AssertJ
Lógica de domínio, regras isoladas e execução em milissegundos"] E2E --> IT IT --> UT

Princípios Norteadores:

  1. Confiança Real: Não usamos bancos de dados em memória (como H2 ou HSQLDB) para validar queries PostgreSQL; testamos sempre contra o PostgreSQL real via Testcontainers.
  2. Isolamento e Determinismo: Cada teste gerencia explicitamente suas fixtures ou reseta suas sequências e tabelas, garantindo que a ordem de execução nunca altere o resultado.
  3. DSL Limpa & Idiomática: O código de teste deve servir como a documentação executável mais legível e agradável do sistema.

3. Nosso Processo de Testes: Guia Passo a Passo da Suíte E2E

Não basta apenas ensinar boas práticas: nós aplicamos engenharia de testes rigorosa no ciclo de vida desta própria plataforma de ensino.

Abaixo, apresentamos a evolução cronológica e arquitetural da nossa suíte de testes End-to-End, desde a concepção inicial até a automação completa com o framework padrão unittest do Python e o Playwright.

flowchart LR P1["3.1 Concepção
(Mapa de Links)"] --> P2["3.2 Ambiente
(Devbox + UV)"] P2 --> P3["3.3 Subindo Hugo & Métricas
(setUpClass)"] P3 --> P4["3.4 Teste de Métricas
(Pages & Aliases)"] P4 --> P5["3.5 Playwright Crawler
(Exaustão do Mapa)"]

3.1 Resumo do Que Vamos Construir

Nosso objetivo é garantir que nenhuma das 158 lições, links de barra lateral, botões Anterior/Próximo ou referências cruzadas quebrem (HTTP 404/500).

Para isso, modelamos o teste em torno de uma estrutura de dados de Grafo com Mapa de Descoberta:

  • Criamos um dicionário links_map: Dict[str, bool] onde a chave é a URL normalizada e o valor booleano indica se ela já foi visitada (True) ou se acabou de ser descoberta e está pendente (False).
  • O teste inicia com as páginas fundamentais no mapa (ex.: /, /packages/, /support/).
  • O navegador carrega cada URL pendente, inspeciona o DOM, encontra todos os links <a> internos e adiciona os inéditos como False.
  • O loop de teste só termina quando 100% dos links descobertos forem navegados com sucesso.
  • Além disso, interceptamos a saída tabular de compilação do Hugo para validar a integridade das métricas do build (Pages, Aliases, Static files).
  • Ao final, as asserções do unittest validam:
    1. self.assertGreaterEqual(hugo_metrics["Pages"], 230) (mínimo de páginas esperadas).
    2. self.assertGreaterEqual(hugo_metrics["Aliases"], 200) (todos os aliases gerados).
    3. self.assertEqual(len(errors), 0) (zero links com erro).
    4. self.assertEqual(total_visitados, total_descobertos) (garantia de exaustão completa).

3.2 Ferramentas e Configuração do Ambiente

Adotamos ferramentas reproduzíveis que eliminam a clássica frase “funciona na minha máquina”:

  1. Devbox (Nix): Fornece o binário isolado do Hugo (v0.166+) sem exigir instalação global no sistema operacional.
  2. UV (Astral): O instalador e executor ultraveloz do ecossistema Python. Com uv run --with playwright, o Playwright e suas dependências são baixados e executados em milissegundos sem poluir o ambiente do desenvolvedor.
  3. Python unittest: A biblioteca padrão do Python, dispensando dependências externas adicionais para o test runner.

Comando para rodar a suíte completa:

1
devbox global run -- uv run --with playwright python3 scripts/test_site_playwright_unittest.py

3.3 Passo 1: Capturando Métricas do Hugo e Ciclo de Vida (setUpClass e tearDownClass)

Durante o build do Hugo, o compilador gera no console uma tabela com a volumetria da geração:

1
2
3
4
5
6
7
8
9
                  │ PT - BR 
──────────────────┼─────────
 Pages            │     231 
 Paginator pages  │       0 
 Non-page files   │       0 
 Static files     │       2 
 Processed images │       0 
 Aliases          │     209 
 Cleaned          │       0 

No método @classmethod setUpClass, nós executamos o build estático preliminar, capturamos o stdout/stderr do processo e extraímos esses valores via expressão regular (re.search) para um dicionário Python reutilizável em todos os métodos de teste:

 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
import unittest
import subprocess
import time
import urllib.request
import re

PORT = 13134
BASE_URL = f"http://localhost:{PORT}"
PROJECT_DIR = "/home/wc/test-with-spring"

class TestHugoSiteE2E(unittest.TestCase):
    hugo_proc = None
    hugo_metrics = {}

    @classmethod
    def parse_hugo_metrics(cls, output: str) -> dict:
        """Extrai as métricas tabulares geradas no console pelo Hugo."""
        metrics = {}
        for line in output.splitlines():
            match = re.search(r'^\s*([A-Za-z\s]+?)\s*│\s*(\d+)', line)
            if match:
                key = match.group(1).strip()
                metrics[key] = int(match.group(2))
        return metrics

    @classmethod
    def setUpClass(cls):
        """Passo 1: Captura métricas do build e sobe o Hugo Server em porta dedicada."""
        print(f"\n[setUpClass] Compilando Hugo para capturar métricas de build...")
        cmd_build = ["devbox", "run", "--", "hugo", "-s", PROJECT_DIR]
        res = subprocess.run(cmd_build, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        cls.hugo_metrics = cls.parse_hugo_metrics(res.stdout + "\n" + res.stderr)
        print(f"[setUpClass] Métricas capturadas: {cls.hugo_metrics}")

        print(f"[setUpClass] Subindo Hugo Server na porta {PORT}...")
        cmd_server = [
            "devbox", "run", "--", "hugo", "server",
            "-s", PROJECT_DIR,
            "-p", str(PORT),
            "--bind", "127.0.0.1",
            "--baseURL", BASE_URL,
            "-D"
        ]
        cls.hugo_proc = subprocess.Popen(
            cmd_server, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True
        )

        # Polling com timeout para garantir que o servidor está pronto
        ready = False
        for _ in range(40):
            time.sleep(0.5)
            try:
                with urllib.request.urlopen(f"{BASE_URL}/", timeout=1) as resp:
                    if resp.status == 200:
                        ready = True
                        break
            except Exception:
                pass

        if not ready:
            raise RuntimeError(f"O servidor Hugo não respondeu a tempo na porta {PORT}")
        print("[setUpClass] Servidor Hugo online e respondendo 200 OK!")

    @classmethod
    def tearDownClass(cls):
        """Passo Final: Garante que o processo seja finalizado sem deixar portas presas."""
        if cls.hugo_proc:
            cls.hugo_proc.terminate()
            try:
                cls.hugo_proc.wait(timeout=3)
            except subprocess.TimeoutExpired:
                cls.hugo_proc.kill()
            print("[tearDownClass] Servidor Hugo finalizado com sucesso.")

3.4 Passo 2: Asserções Sobre as Métricas de Build do Hugo

Agora que as métricas foram extraídas com sucesso, criamos o primeiro teste formal do unittest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    def test_01_hugo_build_metrics(self):
        """Valida que o Hugo gerou todo o volume de páginas e aliases esperado."""
        print(f"\n[Test 1] Validando métricas do Hugo: {self.hugo_metrics}")
        
        # Garante que as 159 aulas + tópicos + pacotes estão presentes
        self.assertIn("Pages", self.hugo_metrics)
        self.assertGreaterEqual(self.hugo_metrics["Pages"], 230)
        
        # Garante que os aliases para compatibilidade com URLs legadas foram compilados
        self.assertIn("Aliases", self.hugo_metrics)
        self.assertGreaterEqual(self.hugo_metrics["Aliases"], 200)

    def test_02_hugo_server_is_up(self):
        """Valida se a home do site está acessível."""
        req = urllib.request.Request(f"{BASE_URL}/")
        with urllib.request.urlopen(req, timeout=5) as resp:
            self.assertEqual(resp.status, 200)

No estágio maduro da aplicação de teste, adicionamos o mecanismo de descoberta recursiva e o mapa de estado links_map. O crawler extrai todos os elementos document.querySelectorAll('a[href]'), filtra âncoras e links externos, e navega progressivamente até zerar as URLs pendentes.

Veja a implementação definitiva presente em scripts/test_site_playwright_unittest.py:

 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
    def test_site_crawler_playwright(self):
        # Mapa de links: URL -> booleano de visitação
        links_map = {
            f"{BASE_URL}/": False,
            f"{BASE_URL}/packages/": False,
            f"{BASE_URL}/support/": False,
            f"{BASE_URL}/cookie-policy/": False,
            f"{BASE_URL}/no-bullshit-privacy-policy/": False,
        }
        errors = []
        iteration = 0

        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            context = browser.new_context()
            page = context.new_page()

            while True:
                # Localiza a próxima URL não visitada
                pending_urls = [u for u, visited in links_map.items() if not visited]
                if not pending_urls:
                    break

                target_url = pending_urls[0]
                iteration += 1

                try:
                    response = page.goto(target_url, wait_until="load", timeout=12000)
                    time.sleep(0.04) # Estabilização de redirects (<meta http-equiv="refresh">)
                    
                    status = response.status if response else 200
                    if status >= 400:
                        errors.append((target_url, f"HTTP {status}"))
                    
                    # Marca como visitado no mapa
                    links_map[target_url] = True
                    
                    # Trata redirecionamentos canônicos de aliases
                    current_url = self.normalize_url(page.url)
                    if current_url:
                        links_map[current_url] = True

                    # Extrai links internos dinamicamente do DOM
                    hrefs = page.evaluate("""() => {
                        const anchors = Array.from(document.querySelectorAll('a[href]'));
                        return anchors.map(a => a.getAttribute('href'));
                    }""")

                    for href in hrefs:
                        if not href or href.startswith(("mailto:", "javascript:")):
                            continue
                        absolute = urljoin(page.url, href)
                        normalized = self.normalize_url(absolute)
                        if normalized and normalized not in links_map:
                            # Novo link descoberto! Adiciona como pendente
                            links_map[normalized] = False

                except Exception as e:
                    errors.append((target_url, str(e)))
                    links_map[target_url] = True

            context.close()
            browser.close()

        total_discovered = len(links_map)
        total_visited = sum(1 for visited in links_map.values() if visited)

        # Asserções rigorosas da biblioteca unittest:
        self.assertEqual(len(errors), 0, f"Erros encontrados: {errors}")
        self.assertEqual(total_visited, total_discovered, "Inconsistência no mapa de links!")

3.6 Resultados da Execução

Ao executar a suíte, o unittest reporta a execução limpa de ponta a ponta:

 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
Info: Running script "uv" on /home/wc/.local/share/devbox/global/default

[setUpClass] Iniciando servidor Hugo na porta 13134...
[setUpClass] Servidor Hugo ativo e pronto para requisições!
.
[Playwright Crawler] Iniciando descoberta e navegação exaustiva de links...
  ✓ [1] [OK] http://localhost:13134/ (Total no mapa: 5)
  ✓ [2] [OK] http://localhost:13134/packages/ (Total no mapa: 9)
  ...
  ✓ [234] [OK] http://localhost:13134/lesson/using-the-same-dbunit-data-sets-from-multiple-maven-modules/

==================================================
RESUMO DA SUÍTE DE TESTES E2E:
Total de links descobertos: 234
Total de links navegados:   234
Erros encontrados:         0
==================================================
.
[tearDownClass] Encerrando processo do Hugo Server na porta 13134...
[tearDownClass] Processo finalizado com sucesso.

----------------------------------------------------------------------
Ran 2 tests in 29.974s

OK

Resumo e Pontos-Chave

  1. O desenvolvimento de software de qualidade começa com uma stack sólida e moderna (Spring Boot 4.1.2, Kotlin 2.4.21, Gradle 9.8.2 e Kotest 6.2.6).
  2. Para testar aplicações e documentações corporativas, construa suítes incrementais: comece validando a prontidão do servidor nos ganchos globais de setup (setUpClass), integre o navegador headless e então expanda para grafos de descoberta com mapas de estado.
  3. Testes End-to-End não precisam ser lentos ou frágeis: com Playwright e Devbox, testamos mais de 230 páginas dinâmicas em menos de 30 segundos com 100% de determinismo.