📚 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
- 01: CRUD básico de jogos · H2 · Bootstrap 5
- 02: Refatoração SOLID/DRY — camada de Service, DTOs, Bean Validation
- 03: Categorias, tema claro/escuro, deploy Render + Neon
- ➡️ 04 (este): Levantamento de Requisitos · Backlog & Sprints · Git Flow · testes · ADRs
🎓 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.`
Partindo do código do Biblioteca de Jogos 03, evoluir o sistema com 3 capacidades pedidas por um usuário real da aplicação.
—`
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.”
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 |
| 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) |
—`
| 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.
| User Story | To Do | Doing | Done |
|---|---|---|---|
| US09 — Busca combinada | |||
| US10 — Jogos represados | |||
| US08 — Exportar CSV |
—`
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
- [ ] `./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
—`
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 comSpecification? Para 3 critérios opcionais, uma única JPQL comOR IS NULLé mais simples de ler e testar do queSpecification/Criteria API — essa última só compensa a complexidade quando os critérios opcionais passam de ~5-6. Isso vira o ADR 007.
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).
@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.
—`
ADR 007 — JPQL com OR IS NULL em vez de Specification/Criteria API
@Query JPQL com (:param IS NULL OR campo = :param) por critério.Specification vira necessário.ADR 008 — Limite de “represado” configurável via application.properties, não hardcoded
app.dias-para-represado como propriedade, lida via @Value, com default de 180 dias.ADR 009 — Exportação reaproveita a query de filtro, não duplica lógica
/jogos/exportar recebe os mesmos parâmetros de / e chama o mesmo jogoRepository.buscar(...).—`
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.
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd javaweb_jogos_04_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`
- As 3 User Stories (US08, US09, US10) estão em branches próprias, PRs mescladas via squash.
./mvnw testpassa, cobrindoJogoAnaliseService.- 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-represadopra um valor baixo (ex.: 1) e confirmar que jogos aparecem como represados.- As 3 decisões técnicas (ADR 007-009) estão registradas em
docs/adr/.`
| 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 | ` |
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"