📚 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:
- Conta no GitHub (para push e GitHub Actions)
- Conta na Render (deploy da API) — plano gratuito disponível
- Conta na Netlify (deploy do frontend) — plano gratuito disponível
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:
- Sucesso no Checkout: Estoque suficiente, redução correta do estoque físico e persistência do pedido.
- Erro no Checkout: Estoque insuficiente, lançamento da exceção
BusinessExceptione 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 nomeRENDER_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) eNETLIFY_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! 🚀