📚 Módulo 09: DevOps - Testes, Docker, GitHub Actions CI/CD e Checklist de Validação

✅ Pré-Requisitos deste Módulo

Confirme antes de começar:

./mvnw clean test    # dentro de backend/ — deve retornar BUILD SUCCESS
npm run build        # dentro de web/ — deve gerar a pasta dist/ sem erros

Você deve ter concluído os Módulos 01–08 completos. Você precisa de:


Neste módulo de encerramento do projeto TecLoja, consolidaremos a infraestrutura de DevOps e qualidade de software para nossa arquitetura de Múltiplos Repositórios (Multirepo).

Implementaremos a esteira de qualidade com testes unitários JUnit 5 e Mockito para a lógica transacional do pedido, definiremos arquivos Dockerfiles otimizados para produção, configuraremos os pipelines automatizados de CI/CD no GitHub Actions (para Render e Netlify) e, finalmente, disponibilizaremos um Checklist de Validação Autoguiado (Rubrica) para garantir que toda a aplicação atende perfeitamente aos requisitos de engenharia e banco de dados.


🗺️ 1. Visão Geral da Pipeline de CI/CD e Deploy Multirepo

Abaixo, ilustramos o fluxo de automação integrado. Cada repositório possui seu próprio ciclo de vida, com testes e deploys independentes na nuvem, garantindo isolamento total do ecossistema.

flowchart TD
    %% Styling
    classDef gitHub fill:#24292e,stroke:#333,stroke-width:2px,color:#fff;
    classDef actions fill:#2088ff,stroke:#005cc5,stroke-width:2px,color:#fff;
    classDef prod fill:#28a745,stroke:#22863a,stroke-width:2px,color:#fff;
    
    subgraph Repositorio_Backend ["📦 Repositório: tecloja-backend"]
        A[Git Push 'main']:::gitHub --> B[GitHub Action CI/CD Backend]:::actions
        B --> C[mvn clean test<br>JUnit 5 + Mockito]:::actions
        B --> D[Docker Build<br>Multi-Stage JDK 17]:::actions
        D --> E[Deploy na Render Container]:::prod
    end

    subgraph Repositorio_Frontend ["📦 Repositório: tecloja-frontend"]
        F[Git Push 'main']:::gitHub --> G[GitHub Action CI/CD Frontend]:::actions
        G --> H[npm run build<br>Angular 18 Standalone]:::actions
        G --> I[Deploy na Netlify CDN]:::prod
    end

    E <-->|CORS Habilitado / JWT Bearer| I

🧪 2. Testes de Unidade com JUnit 5 e Mockito

Em Engenharia de Software, testes de unidade são cruciais para blindar as regras de negócio contra regressões de código. Testaremos a classe PedidoServiceImpl simulando dois cenários determinísticos:

  1. Sucesso no Checkout: Estoque suficiente, redução correta do estoque físico e persistência do pedido.
  2. Erro no Checkout: Estoque insuficiente, lançamento da exceção BusinessException e garantia de não-persistência (conceito teórico de Rollback transacional).

Crie o arquivo src/test/java/br/com/tecloja/api/service/impl/PedidoServiceImplTest.java em seu repositório backend:

package br.com.tecloja.api.service.impl;

import br.com.tecloja.api.dto.ItemPedidoFormDTO;
import br.com.tecloja.api.dto.PedidoDTO;
import br.com.tecloja.api.dto.PedidoFormDTO;
import br.com.tecloja.api.exception.BusinessException;
import br.com.tecloja.api.exception.ResourceNotFoundException;
import br.com.tecloja.api.model.*;
import br.com.tecloja.api.repository.ClienteRepository;
import br.com.tecloja.api.repository.PedidoRepository;
import br.com.tecloja.api.repository.ProdutoRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class PedidoServiceImplTest {

    @Mock
    private PedidoRepository pedidoRepository;

    @Mock
    private ClienteRepository clienteRepository;

    @Mock
    private ProdutoRepository produtoRepository;

    @InjectMocks
    private PedidoServiceImpl pedidoService;

    private Cliente cliente;
    private Produto notebook;
    private PedidoFormDTO pedidoFormDTO;

    @BeforeEach
    void setUp() {
        // Inicializa Cliente Falso
        cliente = new Cliente();
        cliente.setId(1L);
        cliente.setNome("Usuário Padrão");
        cliente.setEmail("usuario@email.com");

        // Inicializa Produto Falso
        notebook = new Produto();
        notebook.setId(10L);
        notebook.setNome("Notebook Gamer");
        notebook.setPreco(new BigDecimal("5000.00"));
        notebook.setEstoque(5); // 5 unidades em estoque

        // Inicializa Formulário de Pedido (Checkout)
        ItemPedidoFormDTO itemForm = new ItemPedidoFormDTO(10L, 2); // Solicita 2 unidades
        pedidoFormDTO = new PedidoFormDTO(1L, List.of(itemForm));
    }

    @Test
    @DisplayName("Deve faturar pedido com sucesso quando houver estoque suficiente")
    void realizarPedido_Sucesso() {
        // Mocks do Repositório
        when(clienteRepository.findById(1L)).thenReturn(Optional.of(cliente));
        when(produtoRepository.findById(10L)).thenReturn(Optional.of(notebook));
        
        // Mock de Salvamento do Pedido (Simula gravação com ID auto-incremento)
        when(pedidoRepository.save(any(Pedido.class))).thenAnswer(invocation -> {
            Pedido p = invocation.getArgument(0);
            p.setId(100L); // Simula ID do banco
            p.getItens().get(0).setId(500L); // Simula ID do item do banco
            return p;
        });

        // Executa a regra
        PedidoDTO resultado = pedidoService.realizarPedido(pedidoFormDTO);

        // Validações
        assertNotNull(resultado);
        assertEquals(100L, resultado.id());
        assertEquals("PAGO", resultado.status());
        assertEquals(1L, resultado.clienteId());
        assertEquals("Usuário Padrão", resultado.clienteNome());
        assertEquals(1, resultado.itens().size());
        assertEquals(new BigDecimal("10000.00"), resultado.valorTotal()); // 2 unidades x R$ 5000.00

        // Valida redução do estoque físico da entidade
        assertEquals(3, notebook.getEstoque()); // 5 - 2 = 3

        // Verifica interações de escrita física no banco
        verify(produtoRepository, times(1)).save(notebook);
        verify(pedidoRepository, times(1)).save(any(Pedido.class));
    }

    @Test
    @DisplayName("Deve lançar BusinessException e não salvar nada no banco se estoque for insuficiente")
    void realizarPedido_EstoqueInsuficiente() {
        // Configura pedido com quantidade que excede o estoque (solicita 6, estoque é 5)
        ItemPedidoFormDTO itemExcedente = new ItemPedidoFormDTO(10L, 6);
        PedidoFormDTO formInvalido = new PedidoFormDTO(1L, List.of(itemExcedente));

        when(clienteRepository.findById(1L)).thenReturn(Optional.of(cliente));
        when(produtoRepository.findById(10L)).thenReturn(Optional.of(notebook));

        // Executa e valida o lançamento da BusinessException
        BusinessException exception = assertThrows(BusinessException.class, () -> {
            pedidoService.realizarPedido(formInvalido);
        });

        assertTrue(exception.getMessage().contains("Estoque insuficiente"));

        // Garante que o estoque original não foi alterado
        assertEquals(5, notebook.getEstoque());

        // Garante que o método save NUNCA foi chamado para produto e pedido (rollback lógico)
        verify(produtoRepository, never()).save(any(Produto.class));
        verify(pedidoRepository, never()).save(any(Pedido.class));
    }

    @Test
    @DisplayName("Deve lançar ResourceNotFoundException se o cliente não existir no banco")
    void realizarPedido_ClienteNaoEncontrado() {
        when(clienteRepository.findById(1L)).thenReturn(Optional.empty());

        assertThrows(ResourceNotFoundException.class, () -> {
            pedidoService.realizarPedido(pedidoFormDTO);
        });

        verify(pedidoRepository, never()).save(any(Pedido.class));
    }
}

🐳 3. Conteinerização com Docker

A conteinerização garante que o código dos alunos rode exatamente no mesmo ambiente controlado na nuvem de produção, independentemente de estarem usando Windows, macOS ou Linux em suas máquinas pessoais.

☕ 1. Dockerfile Otimizado da API (Multistage Build)

O padrão Multi-stage separa o ambiente de compilação (Maven completo) do ambiente de execução final (JRE mínimo). Isso reduz o tamanho final da imagem de ~600MB para apenas ~120MB, acelerando o tempo de boot e diminuindo a superfície de ataques a exploits de segurança.

Crie na raiz do repositório backend o arquivo Dockerfile:

# Estágio 1: Compilação e Build com Maven
FROM maven:3.8.5-openjdk-17-slim AS build
WORKDIR /app

# Copia os arquivos de configuração e dependências primeiro para aproveitar o cache do Docker
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copia o código fonte e realiza o empacotamento pulando testes integrados
COPY src ./src
RUN mvn clean package -DskipTests

# Estágio 2: Ambiente de Execução Ultra-leve (JRE Alpine)
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# Cria usuário não-root para segurança do container em produção
RUN addgroup -S teclojagroup && adduser -S teclojauser -G teclojagroup
USER teclojauser

# Copia apenas o .jar final gerado no estágio 1
COPY --from=build /app/target/*.jar app.jar

# Configurações de ambiente de produção
ENV PORT=8080
EXPOSE 8080

# CORREÇÃO: usa shell form (sh -c) para que ${PORT} seja substituído em runtime.
# O formato exec ["java", ...] não executa pelo shell e ignora variáveis de ambiente.
ENTRYPOINT ["sh", "-c", "java -jar -Dserver.port=${PORT} -Dspring.profiles.active=prod app.jar"]

🅰️ 2. Dockerfile Otimizado do Frontend (Nginx para SPA)

Para o Angular, geramos os arquivos HTML/JS estáticos em produção e os servimos usando o Nginx, um servidor web leve e de altíssima performance para arquivos estáticos.

Crie na raiz do repositório frontend o arquivo Dockerfile:

# Estágio 1: Build da SPA
FROM node:18-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build -- --configuration=production

# Estágio 2: Nginx para servir arquivos estáticos
FROM nginx:alpine

# Copia a configuração customizada do Nginx que suporta rotas SPA (evita erros 404 no refresh)
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copia o build final compilado do Angular para o diretório padrão do Nginx
COPY --from=build /app/dist/tecloja/browser /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

⚙️ Arquivo complementar: nginx.conf

O Angular usa rotas lógicas virtuais. Para que o Nginx não retorne erro 404 quando o usuário recarregar páginas internas (ex: /carrinho), precisamos desse arquivo de configuração direcionando todas as requisições para o index.html.

Crie no frontend o arquivo nginx.conf:

server {
    listen 80;
    server_name localhost;

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

🚀 4. Pipelines CI/CD com GitHub Actions

Configuraremos as automações no GitHub de forma que toda vez que um aluno enviar (push) um código para a branch main, a pipeline rode os testes e atualize o deploy automaticamente.

🛡️ 1. Pipeline Backend (.github/workflows/deploy-backend.yml)

Esta pipeline realiza o checkout, configura o JDK 17, executa a suíte de testes com Maven e dispara um webhook para a Render reconstruir o container com a nova imagem de produção.

Crie no repositório backend a estrutura .github/workflows/deploy-backend.yml:

name: CI/CD Backend - TecLoja API

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Código Fonte
      uses: actions/checkout@v3

    - name: Configurar JDK 17 (Temurin)
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven

    - name: Executar Testes Unitários e Empacotamento
      # Testes usam perfil 'dev' com banco H2 em memória — sem dependência de banco externo
      run: mvn clean package

    - name: Trigger Deploy na Render (Webhook)
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      run: |
        curl -X POST "$"

[!NOTE] No painel da Render, basta criar um Web Service a partir do repositório Docker e copiar a URL do “Deploy Webhook” para as configurações de segredos do GitHub (Settings -> Secrets and Variables -> Actions) com o nome RENDER_DEPLOY_WEBHOOK_URL.


🌐 2. Pipeline Frontend (.github/workflows/deploy-frontend.yml)

Esta pipeline instala as dependências do Node.js, compila o código Angular em modo de produção e faz o deploy estático ultraveloz diretamente para a CDN da Netlify através do CLI oficial.

Crie no repositório frontend a estrutura .github/workflows/deploy-frontend.yml:

name: CI/CD Frontend - TecLoja SPA

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Código Fonte
      uses: actions/checkout@v3

    - name: Configurar Node.js (v18)
      uses: actions/setup-node@v3
      with:
        node-version: 18
        cache: 'npm'

    - name: Instalar Dependências (npm ci)
      run: npm ci

    - name: Compilar SPA para Produção
      run: npm run build -- --configuration=production

    - name: Instalar Netlify CLI
      run: npm install -g netlify-cli

    - name: Deploy de Produção para Netlify
      env:
        NETLIFY_AUTH_TOKEN: $
        NETLIFY_SITE_ID: $
      run: |
        netlify deploy --dir=dist/tecloja/browser --prod --message="Deploy Automático via GitHub Actions - Commit $GITHUB_SHA"

[!TIP] Os segredos NETLIFY_AUTH_TOKEN (gerado nas configurações do seu perfil Netlify) e NETLIFY_SITE_ID (encontrado nas configurações do site recém-criado na Netlify) devem ser adicionados nas credenciais do repositório frontend no GitHub.


📝 5. Checklist de Validação Autoguiado (Auto-avaliação)

Abaixo encontra-se a rubrica técnica e operacional de qualidade de software. Utilize este roteiro prático para autotestar sua própria implementação da TecLoja e diagnosticar possíveis bugs antes de submeter o projeto.

📋 Rubrica de Testes Operacionais:

Item de Avaliação Objetivo do Teste Como Testar Resultado Esperado Status
1. Banco H2 e Seeders Integridade dos dados locais Inicie a API e acesse http://localhost:8080/h2-console. Faça login e rode SELECT COUNT(*) FROM PRODUTOS;. O banco deve possuir 5 produtos eletrônicos cadastrados automaticamente via DataSeeder. [ ]
2. Segurança stateless JWT Bloqueio de acessos não autenticados Faça uma chamada GET no Postman para http://localhost:8080/api/v1/pedidos sem anexar Token. Deve retornar o status 401 Unauthorized com corpo JSON limpo. [ ]
3. Cadastro do Admin Geração e validação de token Envie POST para /api/v1/auth/login com as credenciais do admin criadas no seeder (admin@tecloja.com / admin123). Retorna JSON com o token e dados do usuário contendo o papel ROLE_ADMIN. [ ]
4. Teste de Transação e Rollback Princípio ACID de Banco de Dados Tente fazer um pedido contendo dois itens: um notebook (com 5 unidades em estoque) solicitando 1 unidade, e um fone de ouvido (estoque com 0 unidades) pedindo 1 unidade. O sistema deve recusar o pedido inteiro com 400 Bad Request (“Estoque insuficiente”). Vá no console H2 e garanta que o estoque do notebook continuou sendo 5 (transação sofreu Rollback completo e não salvou nada). [ ]
5. Teste CORS Segurança de origem compartilhada Faça uma chamada a partir de sua aplicação Angular local (http://localhost:4200) para a API backend. A requisição deve ser processada normalmente e sem bloqueios de “CORS Origin Blocked” no console do navegador (graças ao filtro CORS no Spring Security). [ ]
6. Reatividade de Signals Estado reativo da interface cliente Acesse a vitrine, clique 3 vezes no produto “Notebook”. Olhe para a Navbar. O badge do carrinho na Navbar deve atualizar reativamente para (3) sem recarregar a página ou piscar a tela. [ ]
7. Pipeline CI/CD Entrega Contínua Faça uma pequena alteração textual em sua Navbar no Angular e deu git push origin main. A pipeline no GitHub Actions deve acender a luz verde de sucesso e a alteração deve aparecer publicada no link público do Netlify em poucos minutos. [ ]
8. App Mobile (Ionic) Integração mobile com API Execute ionic serve na pasta mobile. Faça login na Tab 2 com credenciais do seeder. O login deve ser efetuado, o token salvo no Capacitor Storage, e a Tab 1 deve listar os produtos reais da API. [ ]

🔍 Checkpoint: Configurando os Secrets do GitHub Actions

Para que os pipelines funcionem, você precisa adicionar os segredos de deploy nos repositórios do GitHub:

Backend (Render)

No seu repositório backend no GitHub, vá em Settings → Secrets and variables → Actions → New repository secret:

Nome do Secret Onde obter
RENDER_API_KEY Render Dashboard → Account Settings → API Keys
RENDER_SERVICE_ID URL do serviço na Render: https://dashboard.render.com/web/srv-XXXXXXX

Frontend (Netlify)

No repositório frontend no GitHub, adicione:

Nome do Secret Onde obter
NETLIFY_AUTH_TOKEN Netlify → User Settings → Applications → Personal access tokens
NETLIFY_SITE_ID Netlify → Site → Site Configuration → Site ID

Verificando a Pipeline

Faça um git push origin main no repositório backend. No GitHub, vá em Actions — você verá o workflow deploy-backend.yml rodando. O ícone deve ficar verde após ~3 minutos.

⚠️ Erros Comuns

Sintoma Causa Solução
GitHub Actions falha em mvn clean test Testes falhando no CI por falta de variáveis Confirme que application.properties não depende de variáveis de ambiente no perfil dev
Deploy na Render falha com Service not found RENDER_SERVICE_ID incorreto Copie o ID diretamente da URL do serviço na Render (formato srv-XXXXXXXX)
Netlify deploy com dist directory not found Pasta de output do Angular diferente Confirme que o comando usa --dir=dist/web/browser (Angular 18 gera subpasta browser/)
Docker build falha em mvn dependency:go-offline Sem conexão com Maven Central durante o build Problema temporário de rede do CI — faça um novo push para reexecutar o workflow
API na Render retorna 500 após deploy Variáveis de ambiente de banco não configuradas No painel da Render, vá em Environment e adicione SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD e JWT_SECRET_KEY
spring.profiles.active=prod não ativado Profile não passado no Dockerfile Confirme ENTRYPOINT ["sh", "-c", "java ... -Dspring.profiles.active=prod app.jar"] no Dockerfile

🏆 Conclusão do Curso

Parabéns! Você concluiu com maestria o desenvolvimento de toda a stack da TecLoja!

Você construiu uma aplicação moderna de ponta a ponta: modelou relações complexas 1:N e N:M em banco de dados, configurou mapeamentos ORM rigorosos com JPA, estruturou uma arquitetura robusta de backend em camadas desacopladas com Spring Boot e JDK 17, implementou segurança baseada em Tokens JWT com criptografia stateless, programou um frontend reativo moderno com Angular 18 Standalone e Signals, empacotou as soluções em imagens otimizadas do Docker e automatizou todo o ciclo de vida via GitHub Actions para Render e Netlify.

Com essa base sólida e profissional, você está perfeitamente preparado para atuar com maestria nos desafios mais complexos de Engenharia de Software e Banco de Dados do mercado! 🚀


Voltar para o Sumário