📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Java e Spring Boot. Recomendado revisar antes de começar.

🎮 Biblioteca de Jogos 04 — Do Backlog Real ao Backlog do Produto

Trilha de Aprendizado — Projeto 4 de 4

🎓 Nível profissional simulado: Sênior. Igual ao Controle de Gastos 04 e ao Login de Usuários 04: você não recebe features prontas, recebe um usuário real reclamando, extrai requisitos, prioriza, e entrega via branches + PR + code review.`

🎯 Objetivo

Partindo do código do Biblioteca de Jogos 03, evoluir o sistema com 3 capacidades pedidas por um usuário real da aplicação.

—`

🧑‍💼 Fase 1 — Levantamento de Requisitos

O Briefing do Usuário

Rafael é um colecionador de jogos que usa a Biblioteca de Jogos 03 para catalogar sua coleção há alguns meses. Ele manda a seguinte mensagem:

“Cara, o sistema já ajuda bastante, mas tenho 3 pedidos. Primeiro: eu queria conseguir baixar minha lista inteira numa planilha, porque toda vez que alguém pergunta ‘quais jogos de RPG você tem’ eu tenho que ficar rolando a tela e copiando manualmente, é um saco. Segundo: eu queria buscar por mais de uma coisa ao mesmo tempo, tipo ‘jogos de Ação, difícil, que eu ainda não zerei’ — hoje só dá pra filtrar por categoria, uma coisa de cada vez. Terceiro, e esse é o motivo de eu ter escrito: outro dia percebi que tenho um jogo aqui comprado há mais de 2 anos e nunca instalei. Isso me deixou putooo comigo mesmo, queria que o sistema me avisasse de jogos assim, os ‘jogos represados’, pra eu não continuar comprando jogo que nunca vou jogar.”

Extraindo Requisitos do Briefing

Requisitos Funcionais (RF):

ID Requisito Origem no briefing
RF01 O sistema deve permitir exportar a lista de jogos (filtrada ou completa) em CSV “baixar minha lista inteira numa planilha”
RF02 O sistema deve permitir buscar/filtrar por múltiplos critérios combinados (categoria + dificuldade + status) “buscar por mais de uma coisa ao mesmo tempo”
RF03 O sistema deve identificar e destacar jogos “represados” (adicionados há mais de N dias e nunca finalizados) “comprado há mais de 2 anos e nunca instalei… queria que o sistema me avisasse”

Requisitos Não-Funcionais (RNF):

ID Requisito Origem no briefing
RNF01 O critério de “represado” (quantos dias) deve ser configurável, não fixo no código implícito — o que é “represado” pra um colecionador casual não é o mesmo pra um colecionador voraz
RNF02 A exportação deve respeitar o filtro ativo no momento, não sempre a lista inteira implícito no RF01 combinado com RF02 — “quais jogos de RPG” sugere exportar o que está filtrado, não tudo

Das Requisitos às User Stories

Requisito User Story
RF01 + RNF02 US08 — Exportar lista (respeitando filtro ativo) em CSV
RF02 US09 — Busca combinada por categoria + dificuldade + status
RF03 + RNF01 US10 — Alerta de jogos represados (limite configurável)

—`

📋 Fase 2 — Backlog do Produto e Sprint Planning

ID User Story Prioridade
US08 Como usuário, quero exportar minha lista filtrada em CSV, para compartilhar ou fazer backup Média
US09 Como usuário, quero combinar categoria + dificuldade + status num único filtro, para encontrar jogos rapidamente Alta
US10 Como usuário, quero ver um alerta de jogos represados, para não continuar comprando o que nunca jogo Alta

Sprint 1: US09 (base do filtro combinado, que US08 e US10 reaproveitam) + US10. Sprint 2: US08.

Quadro Sprint (Kanban)

User Story To Do Doing Done
US09 — Busca combinada      
US10 — Jogos represados      
US08 — Exportar CSV      

—`

🌿 Fase 3 — Git Flow

Mesmo fluxo das trilhas irmãs: branch feature/US0Xx-nome por User Story, commit semântico, PR com checklist, squash merge.

git checkout main && git pull origin main
git checkout -b feature/US09-busca-combinada
# ... implementa, testa, commita ...
git push -u origin feature/US09-busca-combinada
# abre PR, revisa com checklist, squash merge
git checkout main && git pull origin main && git branch -d feature/US09-busca-combinada

Checklist de Pull Request

- [ ] `./mvnw test` passa
- [ ] Testei o filtro combinado com 0, 1 e os 3 critérios preenchidos ao mesmo tempo
- [ ] Se tomei uma decisão técnica relevante, registrei um ADR

—`

🛠️ Fase 4 — Implementação

US09 — Busca Combinada

JogoRepository ganha uma consulta com critérios opcionais via JPQL:

@Query("""
    SELECT j FROM Jogo j WHERE
    (:categoriaId IS NULL OR j.categoria.id = :categoriaId) AND
    (:dificuldade IS NULL OR j.dificuldade = :dificuldade) AND
    (:finalizado IS NULL OR j.finalizado = :finalizado)
    """)
Page<Jogo> buscar(@Param("categoriaId") Long categoriaId,
                   @Param("dificuldade") Dificuldade dificuldade,
                   @Param("finalizado") Boolean finalizado,
                   Pageable pageable);

💡 Por que :categoriaId IS NULL OR ... em vez de montar a query dinamicamente com Specification? Para 3 critérios opcionais, uma única JPQL com OR IS NULL é mais simples de ler e testar do que Specification/Criteria API — essa última só compensa a complexidade quando os critérios opcionais passam de ~5-6. Isso vira o ADR 007.

US10 — Alerta de Jogos Represados

JogoAnaliseService — lógica pura, extraída para ser testável:

package br.com.bibliotecajogos.service;

import br.com.bibliotecajogos.entity.Jogo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;

@Service
public class JogoAnaliseService {

    @Value("${app.dias-para-represado:180}")
    private int diasParaRepresado;

    public boolean estaRepresado(Jogo jogo) {
        if (jogo.isFinalizado()) return false;
        long dias = ChronoUnit.DAYS.between(jogo.getDataAdicionado(), LocalDate.now());
        return dias >= diasParaRepresado;
    }

    public List<Jogo> filtrarRepresados(List<Jogo> jogos) {
        return jogos.stream().filter(this::estaRepresado).toList();
    }
}

application.properties: app.dias-para-represado=180 (RNF01 — configurável, não fixo no código).

US08 — Exportar CSV (respeitando o filtro)

@GetMapping("/jogos/exportar")
public ResponseEntity<String> exportarCsv(@RequestParam(required = false) Long categoriaId,
                                           @RequestParam(required = false) Dificuldade dificuldade,
                                           @RequestParam(required = false) Boolean finalizado) {
    List<Jogo> jogos = jogoRepository.buscar(categoriaId, dificuldade, finalizado, Pageable.unpaged()).getContent();

    StringBuilder csv = new StringBuilder("Titulo;Categoria;Dificuldade;Nota;Finalizado\n");
    for (Jogo j : jogos) {
        csv.append(j.getTitulo()).append(";")
           .append(j.getCategoria() != null ? j.getCategoria().getNome() : "").append(";")
           .append(j.getDificuldade()).append(";")
           .append(j.getNota()).append(";")
           .append(j.isFinalizado()).append("\n");
    }

    return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename=biblioteca-jogos.csv")
            .header("Content-Type", "text/csv; charset=UTF-8")
            .body(csv.toString());
}

O endpoint recebe os mesmos parâmetros de filtro da tela principal — se o usuário está vendo só os RPGs difíceis não-zerados, exportar gera exatamente essa lista (RNF02), não a biblioteca inteira.

—`

🧭 Decisões Técnicas (ADRs)

ADR 007 — JPQL com OR IS NULL em vez de Specification/Criteria API

ADR 008 — Limite de “represado” configurável via application.properties, não hardcoded

ADR 009 — Exportação reaproveita a query de filtro, não duplica lógica

—`

🧪 Testes Automatizados

class JogoAnaliseServiceTest {

    private final JogoAnaliseService service = new JogoAnaliseService();
    // diasParaRepresado teria que ser setado via reflection ou construtor em um teste real com @Value;
    // aqui assumimos o default de 180 dias para simplificar o exemplo didático.

    private Jogo criarJogo(LocalDate dataAdicionado, boolean finalizado) {
        Jogo jogo = new Jogo();
        jogo.setDataAdicionado(dataAdicionado);
        jogo.setFinalizado(finalizado);
        return jogo;
    }

    @Test
    void estaRepresado_deveSerFalsoSeJaFinalizado() {
        Jogo jogo = criarJogo(LocalDate.now().minusDays(300), true);
        assertFalse(service.estaRepresado(jogo));
    }

    @Test
    void estaRepresado_deveSerFalsoSeAdicionadoRecentemente() {
        Jogo jogo = criarJogo(LocalDate.now().minusDays(10), false);
        assertFalse(service.estaRepresado(jogo));
    }

    @Test
    void filtrarRepresados_deveRetornarSoOsRepresados() {
        Jogo antigo = criarJogo(LocalDate.now().minusDays(400), false);
        Jogo novo = criarJogo(LocalDate.now().minusDays(5), false);

        List<Jogo> resultado = service.filtrarRepresados(List.of(antigo, novo));

        assertEquals(1, resultado.size());
    }
}

💡 Por que o teste de “já finalizado” vem primeiro? Um jogo comprado há 3 anos mas zerado no mês seguinte não é “represado” — é só um jogo antigo. Esse teste documenta que a regra de negócio real é “esquecido”, não “antigo”. Sem esse teste, um refactor futuro poderia remover o if (jogo.isFinalizado()) achando que é redundante, e o alerta passaria a soar para jogos que na verdade já foram jogados.


—`

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd javaweb_jogos_04_testes

2. Execute a aplicação e os testes

./mvnw spring-boot:run
./mvnw test

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/javaweb_jogos_04_testes`

✅ Checkpoint Final

  1. As 3 User Stories (US08, US09, US10) estão em branches próprias, PRs mescladas via squash.
  2. ./mvnw test passa, cobrindo JogoAnaliseService.
  3. Testou manualmente: filtrar por categoria+dificuldade+status ao mesmo tempo; exportar CSV com um filtro ativo e conferir que só os jogos filtrados aparecem; mudar app.dias-para-represado pra um valor baixo (ex.: 1) e confirmar que jogos aparecem como represados.
  4. As 3 decisões técnicas (ADR 007-009) estão registradas em docs/adr/.`

🏆 Conclusão da Trilha (4 de 4)

Projeto Nível simulado Foco  
01 Estagiário CRUD básico guiado, controller falando direto com o repositório  
02 Júnior Refatoração SOLID/DRY — Service layer, DTOs, Bean Validation  
03 Pleno Categorias, paginação, validação real, tema claro/escuro, deploy  
04 Sênior Processo: requisitos, backlog, Git Flow, testes, ADRs `

📋 Resumo de Comandos

git checkout main && git pull origin main
git checkout -b feature/US0Xx-nome-curto
git add . && git commit -m "feat: descricao (US0Xx)"
git push -u origin feature/US0Xx-nome-curto
git checkout main && git pull origin main && git branch -d feature/US0Xx-nome-curto

./mvnw test
cd bibliotecajogos && ./mvnw spring-boot:run

# Testar a exportacao com filtro
curl -o biblioteca.csv "http://localhost:8080/jogos/exportar?dificuldade=DIFICIL&finalizado=false"

Voltar para Projetos