Projeto cafe fullstack Django e Vue.js

Criação de projeto de cafe fullstack Django, Vue.js, Postman e Bootstrap
Voltar

Material complementar:


Conceito:

Criar sistema web RESTful fullstack, com backend Django REST Framework - DRF (viewsets e routers) e frontend Vue.js TypeScript (vue router), com funções de CRUD (criar, listar, ver específico, editar e excluir) de cafés Parisienses. Testes através do Postman, estilização frontend através do Bootstrap (Bootstrap vue next). Possui os projetos "paris_cafe" (backend Django REST Framework) e "cafe_frontend" (frontend Vue.js TypeScript) integrados como sistema RESTful fullstack.


Backend (Django REST Framework):

  1. Criar projeto DRF:
    
    pip3 install django djangorestframework markdown django-filter django-cors-headers pytest pytest-django pytest-html model_bakery --break-system-packages
    django-admin startproject paris_cafe
    cd paris_cafe
    python manage.py startapp cafes
    
  2. Em paris_cafe/settings.py, informar conteúdo faltante:
    
    INSTALLED_APPS = [
        # Outro conteúdo
        'rest_framework',
        'corsheaders',
        'cafes',
    ]
    
    MIDDLEWARE = [
        # Outro conteúdo
        'corsheaders.middleware.CorsMiddleware',
    ]
    
    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
        ],
        'DEFAULT_PARSER_CLASSES': [
            'rest_framework.parsers.JSONParser',
        ]
    }
    
    CORS_ALLOW_ALL_ORIGINS = True
    # CORS_ALLOWED_ORIGINS = [
    #    "http://localhost:8080",
    # ]
    
  3. Em cafes/models.py, criar Model Cafeteria:
    
    from django.db import models
    from django.core.validators import MinValueValidator, MaxValueValidator
    
    class Cafeteria(models.Model):
        nome = models.CharField(max_length=255)
        data_inauguracao = models.DateField()
        endereco = models.CharField(max_length=255)
        avaliacao = models.FloatField(validators=[MinValueValidator(0.0), MaxValueValidator(10.0)])
    
        def __str__(self):
            return f"{self.nome} ({self.avaliacao}/10)"
    
  4. Em cafes/serializers.py, criar arquivo de serializadores:
    
    from rest_framework import serializers
    from .models import Cafeteria
    
    class CafeteriaSerializer(serializers.ModelSerializer):
        class Meta:
            model = Cafeteria
            fields = '__all__'
    
  5. Em cafes/views.py, criar viewsets:
    
    from rest_framework import viewsets
    from .models import Cafeteria
    from .serializers import CafeteriaSerializer
    
    class CafeteriaViewSet(viewsets.ModelViewSet):
        queryset = Cafeteria.objects.all()
        serializer_class = CafeteriaSerializer
    
  6. Em cafes/urls.py, criar arquivo de routes locais:
    
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from .views import CafeteriaViewSet
    
    router = DefaultRouter()
    router.register(r'cafes', CafeteriaViewSet)
    
    urlpatterns = [
        path('', include(router.urls)),
    ]
    
  7. Em paris_cafe/urls.py, editar arquivo de routes globais:
    
    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('cafes.urls')),
    ]
    
  8. Executar projeto:
    
    python manage.py makemigrations
    python manage.py migrate
    python manage.py runserver
    
  9. Acessar projeto: http://localhost:8000/cafes/

Testes no Postman:

  1. Listar cafeterias (GET list):
  2. Ver cafeteria 1 (GET detail):
  3. Adicionar cafeteria (POST create):
    • URL: http://localhost:8000/cafes/
    • Método: POST
    • Body (JSON):
      
      {
          "nome": "Les Deux Magots",
          "data_inauguracao": "1884-01-01",
          "endereco": "6 Pl. Saint-Germain des Prés, 75006 Paris, França",
          "avaliacao": 9.8
      }
      
    • Send (enviar)
  4. Editar cafeteria 1 (PUT update):
    • URL: http://127.0.0.1:/cafes/1/
    • Método: PUT
    • Body (JSON):
      
      {
          "nome": "Les Deux Magots ATUALIZADO",
          "data_inauguracao": "1884-01-01",
          "endereco": "6 Pl. Saint-Germain des Prés, 75006 Paris, França",
          "avaliacao": 9.8
      }
                                  
    • Send (enviar)
  5. Excluir cafeteria 1 (DELETE):

Frontend (Vue.js):

  1. Criar projeto Vue.js:
    
    npm install -g @vue/cli
    npm init vue@latest => Nome: cafe-frontend, Escolha: TypeScript, Router (SPA development)
    cd cafe-frontend
    npm install axios bootstrap bootstrap-vue-next --save-dev cypress ts-node @cypress/webpack-dev-server vitest @vitest/ui jsdom @vue/test-utils
    
  2. Em src/components/CafeItem.vue, criar componente item de cafeteria:
    
    <template>
        <b-card class="mb-2">
            <div class="d-flex justify-content-between align-items-center">
                <div>
                    <h5>{{ cafe.nome }}</h5>
                    <p class="mb-0">Avaliação: {{ cafe.avaliacao }}/10</p>
                </div>
                <div>
                    <router-link :to="`/view/${cafe.id}`" class="btn btn-sm btn-primary me-1">Ver</router-link>
                    <router-link :to="`/edit/${cafe.id}`" class="btn btn-sm btn-warning me-1">Editar</router-link>
                    <b-button size="sm" variant="danger" @click="$emit('delete', cafe.id)">Deletar</b-button>
                </div>
            </div>
        </b-card>
    </template>
    
    <script setup lang="ts">
    defineProps<{ cafe: any }>()
    defineEmits(['delete'])
    </script>
    
  3. Em src/router/index.ts, adicionar rotas:
    
    import { createRouter, createWebHistory } from 'vue-router'
    import Home from '../views/Home.vue'
    import AddCafe from '../views/AddCafe.vue'
    import EditCafe from '../views/EditCafe.vue'
    import ViewCafe from '../views/ViewCafe.vue'
    
    const routes = [
        { path: '/', name: 'Home', component: Home },
        { path: '/add', name: 'AddCafe', component: AddCafe },
        { path: '/edit/:id', name: 'EditCafe', component: EditCafe },
        { path: '/view/:id', name: 'ViewCafe', component: ViewCafe }
    ]
    
    export default createRouter({
        history: createWebHistory(),
        routes
    })
    
  4. Em src/services/api.ts, informar URL API backend:
    
    import axios from 'axios'
    
    const api = axios.create({
        baseURL: 'http://127.0.0.1:8000/',
    })
    
    export default api
    
  5. Em src/views/AddCafe.vue, criar tela de cadastro de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2>Adicionar Cafeteria:</h2>
            <b-form @submit.prevent="addCafe">
                <b-form-input v-model="form.nome" placeholder="Nome" name="nome" class="mb-2" maxlength="255" required />
                <b-form-input v-model="form.data_inauguracao" type="date" name="data_inauguracao" class="mb-2" :max="new Date().toLocaleDateString('sv-SE')" required />
                <b-form-textarea v-model="form.endereco" placeholder="Endereço" name="endereco" class="mb-2" maxlength="255" required />
                <b-form-input v-model.number="form.avaliacao" type="number" step="0.01" min="0" max="10" name="avaliacao" class="mb-2"
                    required />
                <b-button type="submit" variant="success" class="me-2">Confirmar</b-button>
                <router-link to="/" class="btn btn-secondary">Cancelar</router-link>
            </b-form>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import { useRouter } from 'vue-router'
    import api from '../services/api'
    
    const router = useRouter()
    const form = ref({ nome: '', data_inauguracao: '', endereco: '', avaliacao: 0 })
    
    const addCafe = async () => {
        await api.post('cafes/', form.value)
        router.push('/')
    }
    </script>
    
  6. Em src/views/EditCafe.vue, criar tela edição de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2>Editar Cafeteria {{ form.id }}:</h2>
            <b-form @submit.prevent="updateCafe">
                <b-form-input v-model="form.nome" name="nome" class="mb-2" maxlength="255" required />
                <b-form-input v-model="form.data_inauguracao" type="date" name="data_inauguracao" class="mb-2" :max="new Date().toLocaleDateString('sv-SE')" required />
                <b-form-textarea v-model="form.endereco" name="endereco" class="mb-2" maxlength="255" required />
                <b-form-input v-model.number="form.avaliacao" type="number" step="0.01" min="0" max="10" name="avaliacao" class="mb-2"
                    required />
                <b-button type="submit" variant="success" class="btn btn-success me-2">Confirmar</b-button>
                <router-link to="/" class="btn btn-secondary">Cancelar</router-link>
            </b-form>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import { useRoute, useRouter } from 'vue-router'
    import api from '../services/api'
    
    const route = useRoute()
    const router = useRouter()
    const form = ref<any>({})
    
    onMounted(async () => {
        const { data } = await api.get(`cafes/${route.params.id}/`)
        form.value = data
    })
    
    const updateCafe = async () => {
        await api.put(`cafes/${route.params.id}/`, form.value)
        router.push('/')
    }
    </script>
    
  7. Em src/views/Home.vue, criar tela home:
    
    <template>
        <div class="container mt-4">
            <div class="d-flex justify-content-between align-items-center mb-4">
                <h1>Cafés de Paris</h1>
                <div>
                    <router-link to="/add" class="btn btn-success">Adicionar Cafeteria</router-link>
                </div>
            </div>
            <button class="btn btn-sm btn-info me-2 mb-2" @click="toggleOrdenacao">
                {{ ordenado ? 'Ordenação original' : 'Ordenar por avaliação' }}
            </button>
            <CafeItem v-for="cafe in cafes" :key="cafe.id" :cafe="cafe" @delete="deleteCafe" />
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import api from '../services/api'
    import CafeItem from '../components/CafeItem.vue'
    
    const cafes = ref<any[]>([])
    const cafesOriginais = ref<any[]>([])
    const ordenado = ref(false)
    
    const fetchCafes = async () => {
        const { data } = await api.get('cafes/')
        cafesOriginais.value = [...data]
        cafes.value = [...data]
    }
    
    const deleteCafe = async (id: number) => {
        await api.delete(`cafes/${id}/`)
        await fetchCafes()
        if (ordenado.value) {
            ordenarPorAvaliacao()
        }
    }
    
    const ordenarPorAvaliacao = () => {
        cafes.value = [...cafes.value].sort((a, b) => b.avaliacao - a.avaliacao)
    }
    
    const toggleOrdenacao = () => {
        if (!ordenado.value) {
            ordenarPorAvaliacao()
        } else {
            cafes.value = [...cafesOriginais.value]
        }
        ordenado.value = !ordenado.value
    }
    
    onMounted(fetchCafes)
    </script>
    
  8. Em src/views/ViewCafe.vue, criar tela visualização de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2 class="mb-3">Cafeteria {{ cafe?.id }}:</h2>
            <p><strong>Nome:</strong> {{ cafe?.nome }}</p>
            <p><strong>Inauguração:</strong> {{ cafe?.data_inauguracao.split('-').reverse().join('/') }}</p>
            <p><strong>Endereço:</strong> {{ cafe?.endereco }}</p>
            <p><strong>Avaliação:</strong> {{ cafe?.avaliacao }}/10</p>
            <router-link to="/" class="btn btn-secondary mt-2">Voltar</router-link>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import { useRoute } from 'vue-router'
    import api from '../services/api'
    
    const cafe = ref<any>(null)
    const route = useRoute()
    
    onMounted(async () => {
        const { data } = await api.get(`cafes/${route.params.id}/`)
        cafe.value = data
    })
    </script>
    
  9. Em src/App.vue, criar index do projeto:
    
    <template>
        <router-view />
    </template>
    
    <script setup lang="ts"></script>
    
    <style></style>
    
  10. Em src/main.ts, configurar dependências:
    
    import { createApp } from "vue";
    import App from './App.vue';
    import router from './router';
    import * as BootstrapVueNext from 'bootstrap-vue-next';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
    
    import {
        BButton,
        BForm,
        BFormInput,
        BFormTextarea,
        BCard
    } from 'bootstrap-vue-next';
    
    const app = createApp(App);
    app.use(router);
    app.component('BButton', BButton);
    app.component('BForm', BForm);
    app.component('BFormInput', BFormInput);
    app.component('BFormTextarea', BFormTextarea);
    app.component('BCard', BCard);
    app.mount('#app');
    
  11. Executar projeto: npm run dev # ou 'npx vite'

Testes backend:

  • Teste unitário: verifica se unidade isolada do código (como função ou método) funciona corretamente;
  • Teste de integração: verifica se diferentes partes do sistema funcionam juntas corretamente;
  • Teste E2E (end-to-end): verifica se sistema funciona como um todo, simulando comportamento ao usuário final.

Testes unitários e de integração, via pytest.

  1. Pasta do projeto backend, criar arquivo pytest.ini:
    
    [pytest]
    DJANGO_SETTINGS_MODULE = paris_cafe.settings
    python_files = tests.py test_*.py *_tests.py
    
  2. Em pasta App 'cafes', criar diretório tests/
  3. Em tests/, criar arquivo conftest.py para uso de fixtures compartilhadas:
    
    import pytest
    from model_bakery import baker
    
    @pytest.fixture
    def cafe():
        return baker.make("cafes.Cafeteria") # 'cafes' é nome da App do projeto
    
  4. Em tests/, criar diretório unit/ para testes unitários
  5. Em tests/unit/, criar arquivo test_models.py para testar models:
    
    import pytest
    from cafes.models import Cafeteria
    
    @pytest.mark.django_db
    def test_cria_cafeteria():
        cafe = Cafeteria.objects.create(
            nome="Café do Centro",
            data_inauguracao="2020-01-01",
            endereco="Rua Principal, 123",
            avaliacao=9.5,
        )
        assert cafe.nome == "Café do Centro"
        assert cafe.avaliacao == 9.5
    
  6. Em tests/unit/, criar arquivo test_serializers.py para testar serializers:
    
    from cafes.serializers import CafeteriaSerializer
    
    def test_cafe_serializer_valid_data():
        data = {
            "nome": "Café Central",
            "data_inauguracao": "2021-01-01",
            "endereco": "Av. Principal, 456",
            "avaliacao": 8.7
        }
        serializer = CafeteriaSerializer(data=data)
        assert serializer.is_valid()
        assert serializer.validated_data["nome"] == "Café Central"
    
  7. Em tests/, criar diretório integration/ para testes de integração
  8. Em tests/integration/, criar arquivo test_views.py para testar views:
    
    import pytest
    from rest_framework.test import APIClient
    from model_bakery import baker
    
    @pytest.mark.django_db
    def test_lista_cafeterias():
        baker.make("cafes.Cafeteria", _quantity=3)
        client = APIClient()
        response = client.get("/cafes/")
        assert response.status_code == 200
        assert len(response.data) == 3
    
    @pytest.mark.django_db
    def test_cria_cafeteria_api():
        client = APIClient()
        data = {
            "nome": "API Café",
            "data_inauguracao": "2023-06-15",
            "endereco": "Avenida API, 789",
            "avaliacao": 7.2,
        }
        response = client.post("/cafes/", data=data, format='json')
        assert response.status_code == 201
        assert response.data["nome"] == "API Café"
    
  9. Executar testes:
    • Via terminal CLI: pytest
    • Via terminal CLI com relatório HTML: pytest --html=relatorio.html
      • Abrir no Browser: start relatorio.html

Testes frontend:

Testes unitários e de integração, via test runner Vitest.

  1. Em src/, criar diretórios tests/unit/ para testes unitários
  2. Modificar vite.config.ts:
    
    import { fileURLToPath, URL } from 'node:url'
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import vueDevTools from 'vite-plugin-vue-devtools'
    
    export default defineConfig({
        plugins: [
            vue(),
            vueDevTools(),
        ],
        resolve: {
            alias: {
                '@': fileURLToPath(new URL('./src', import.meta.url))
            },
        },
        test: {
            globals: true,
            environment: 'jsdom',
            setupFiles: ['./src/tests/setup.ts'],
        },
    })
    
  3. Em package.json, adicionar trecho de script de testes:
    
    "scripts": {
        # Outro conteúdo
        "test": "vitest",
        "test:ui": "vitest --ui",
        "test:e2e": "cypress run --config-file cypress.config.ts --env TS_NODE_COMPILER_OPTIONS='{\"module\":\"ESNext\"}'"
    }
    
  4. Em src/tests/setup.ts, informar configurações de arquivos de testes:
    
    import { beforeAll } from 'vitest'
    
    beforeAll(() => {
        Object.defineProperty(window, 'matchMedia', {
            writable: true,
            value: (query: string) => ({
                matches: false,
                media: query,
                onchange: null,
                addListener: () => { },
                removeListener: () => { },
                addEventListener: () => { },
                removeEventListener: () => { },
                dispatchEvent: () => false,
            }),
        })
    })
    
  5. Editar tsconfig.json:
    
    {
        "files": [],
        "references": [
            {
                "path": "./tsconfig.node.json"
            },
            {
                "path": "./tsconfig.app.json"
            }
        ],
        "compilerOptions": {
            "target": "ES2022",
            "module": "ESNext",
            "moduleResolution": "Bundler",
            "types": [
                "cypress"
            ],
            "esModuleInterop": true,
            "moduleDetection": "force",
            "allowSyntheticDefaultImports": true,
            "resolveJsonModule": true,
            "strict": true
        },
        "include": [
            "cypress/**/*.ts"
        ]
    }
    
  6. Em src/tests/unit/ViewCafe.spec.ts, criar arquivo de testes unitários para tela ViewCafe:
    
    import { mount } from '@vue/test-utils'
    import ViewCafe from '../../views/ViewCafe.vue'
    import { describe, it, expect, vi } from 'vitest'
    import { createMemoryHistory, createRouter } from 'vue-router'
    import api from '@/services/api'
    
    vi.mock('@/services/api')
    
    const mockCafe = {
        id: 1,
        nome: 'Café de Flore',
        data_inauguracao: '1887-01-01',
        endereco: '172 Boulevard Saint-Germain, Paris',
        avaliacao: 9.5,
    }
    
    const router = createRouter({
        history: createMemoryHistory(),
        routes: [{ path: '/view/:id', component: ViewCafe }],
    })
    
    describe('ViewCafe.vue', () => {
        it('exibe detalhes da cafeteria', async () => {
            (api.get as any).mockResolvedValue({ data: mockCafe })
    
            router.push('/view/1')
            await router.isReady()
    
            const wrapper = mount(ViewCafe, {
                global: {
                    plugins: [router],
                },
            })
    
            await new Promise(resolve => setTimeout(resolve, 0))
            expect(wrapper.text()).toContain('Café de Flore')
            expect(wrapper.text()).toContain('1887')
            expect(wrapper.text()).toContain('172 Boulevard Saint-Germain')
            expect(wrapper.text()).toContain('9.5')
        })
    })
    
  7. Em tests/, criar diretório integration/ para testes de integração
  8. Em tests/integration/, criar arquivo ViewCafe.integration.spec.ts para teste de exibição de detalhes da cafeteria:
    
    import { mount } from '@vue/test-utils'
    import ViewCafe from '../../views/ViewCafe.vue'
    import api from '@/services/api'
    import { describe, it, expect, vi } from 'vitest'
    
    const mockCafe = {
        id: 1,
        nome: 'Café Teste',
        data_inauguracao: '2023-07-20',
        endereco: 'Rua Teste, 123',
        avaliacao: 8.5,
    }
    
    vi.mock('@/services/api', () => ({
        default: {
            get: vi.fn(() => Promise.resolve({ data: mockCafe })),
        },
    }))
    
    vi.mock('vue-router', () => ({
        useRoute: () => ({
            params: { id: '1' },
        }),
    }))
    
    describe('ViewCafe - teste de integração', () => {
        it('exibe os detalhes da cafeteria corretamente', async () => {
            const wrapper = mount(ViewCafe, {
                global: {
                    stubs: ['router-link'],
                },
            })
    
            await wrapper.vm.$nextTick()
            await wrapper.vm.$nextTick()
    
            expect(wrapper.text()).toContain('Café Teste')
            expect(wrapper.text()).toContain('20/07/2023')
            expect(wrapper.text()).toContain('Rua Teste, 123')
            expect(wrapper.text()).toContain('8.5')
        })
    })
    
  9. Executar testes:
    • Todos testes via terminal CLI: npx vitest
    • Todos testes com relatório em HTML: npx vitest run --reporter=html
    • Testes unitários via terminal CLI: npx vitest run tests/unit
    • Testes de integração via terminal CLI: npx vitest --run tests/integration
    • Todos testes via interface gráfica GUI: npx vitest --ui
    • Testes unitários via interface gráfica GUI: npx vitest --ui tests/unit
    • Testes de integração via interface gráfica GUI: npx vitest --ui tests/integration

Testes E2E:

Testes End-to-End (E2E) para validar fluxo completo da aplicação RESTful fullstack, via Cypress.

  1. Pré-requisitos: executar servidores backend e frontend;
  2. Pasta raíz do projeto frontend, criar estrutura de pastas cypress e abrir Cypress GUI: npx cypress open
  3. Pasta raíz do projeto frontend, editar arquivo 'cypress.config.ts':
    
    import { defineConfig } from 'cypress'
    
    export default defineConfig({
        e2e: {
            baseUrl: 'http://localhost:5173',
            specPattern: 'cypress/e2e/**/*.cy.{js,ts}',
            supportFile: 'cypress/support/e2e.ts',
            viewportWidth: 1280,
            viewportHeight: 800,
            defaultCommandTimeout: 8000,
            video: true,
        },
    })
    
  4. Em frontend/cypress, criar diretório e2e/ para arquivos de testes E2E
  5. Em frontend/cypress/e2e/, criar arquivo view_cafe.cy.ts para teste E2E da página de detalhes da cafeteria:
    
    describe('Visualização de uma cafeteria', () => {
        it('deve exibir os detalhes corretamente', () => {
            cy.visit('http://localhost:5173/view/1')
            cy.contains('Cafeteria 1:')
            cy.contains('Nome:')
            cy.contains('Avaliação:')
            cy.contains('/10')
            cy.get('a').contains('Voltar').click()
            cy.url().should('eq', 'http://localhost:5173/')
        })
    })
    
  6. Executar testes: npx cypress run (em package.json, remover linha: type: "module")
    • Via terminal CLI: npx cypress run
    • Via interface gráfica GUI: npx cypress open

Pipelines CI/CD:

Configuração de pipelines (fluxos automatizadores) de integração contínua (CI) e entrega contínua (CD) para automatizar testes e deploy no GitHub Actions. Criação de repositório GitHub para armazenar projetos backend (paris_cafe) e frontend (cafe-frontend). Conteúdo do repositório "prj_cafe_fullstack", contendo Pipelines backend.yml e frontend.yml:

  • Repositório prj_cafe_fullstack
    • paris_cafe/
      • requirements.txt # junto ao manage.py
    • cafe-frontend/
    • .github/
      • workflows/
        • backend.yml
        • frontend.yml
  1. Criar conta no GitHub, cadastrar usuário e email no Git, vinculá-los a chave SSH .pub, e cadastrar chave no GitHub;
  2. No GitHub, criar repositório "prj_cafe_fullstack", e baixá-lo com git clone;
  3. Mover projetos paris_cafe e cafe-frontend para dentro da pasta do repositório "prj_cafe_fullstack";
  4. Em prj_cafe_fullstack, criar diretório .github/workflows/;
  5. Em paris_cafe/requirements.txt, informar dependências do backend:
    
    Django
    djangorestframework
    markdown
    django-filter
    django-cors-headers
    pytest
    pytest-django
    pytest-html
    model_bakery
    
  6. Em .github/workflows/, criar arquivo Pipeline backend.yml:
    
    name: Django Backend CI
    
    on:
        push:
            branches: [main]
        pull_request:
            branches: [main]
    
    jobs:
        testes-backend:
            runs-on: ubuntu-latest
    
            services:
                postgres:
                    image: postgres:15
                    env:
                        POSTGRES_USER: postgres
                        POSTGRES_PASSWORD: postgres
                        POSTGRES_DB: test_db
                    ports: ["5432:5432"]
                    options: >-
                        --health-cmd pg_isready
                        --health-interval 10s
                        --health-timeout 5s
                        --health-retries 5
    
            env:
                DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                DJANGO_SETTINGS_MODULE: paris_cafe.settings
                PYTHONUNBUFFERED: 1
    
            steps:
                - name: Checkout o código
                  uses: actions/checkout@v4
    
                - name: Instalar Python
                  uses: actions/setup-python@v5
                  with:
                      python-version: "3.11"
    
                - name: Instalar dependências
                  working-directory: paris_cafe
                  run: |
                      python -m pip install --upgrade pip
                      pip install -r requirements.txt
    
                - name: Migrar banco de dados
                  working-directory: paris_cafe
                  run: |
                      python manage.py migrate
    
                - name: Executar testes com pytest
                  working-directory: paris_cafe
                  run: |
                      pytest --html=report.html
    
                - name: Upload do relatório de testes
                  uses: actions/upload-artifact@v4
                  with:
                      name: pytest-report
                      path: paris_cafe/report.html
    
  7. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Projetos inseridos e Pipeline backend criada"
    • git push
    • Conferir: git status
  8. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-backend" foi criada e suas etapas executadas corretamente;
  9. Em .github/workflows/, criar arquivo Pipeline frontend.yml:
    
    name: Pipeline Frontend CI
    
    on:
        push:
            branches:
                - main
                - develop
        pull_request:
    
    jobs:
        testes-frontend:
            runs-on: ubuntu-latest
            steps:
                - name: Checkout do código
                  uses: actions/checkout@v3
    
                - name: Instalar Node.js
                  uses: actions/setup-node@v3
                  with:
                      node-version: "22"
    
                - name: Instalar dependências
                  run: npm install
                  working-directory: cafe-frontend
    
                - name: Executar testes unitários e de integração
                  run: npx vitest
                  working-directory: cafe-frontend
    
                - name: Gerar relatório de testes
                  run: npx vitest --reporter=html
                  working-directory: cafe-frontend
    
  10. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Pipeline frontend criada"
    • git push
    • Conferir: git status
  11. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-frontend" foi criada e suas etapas executadas corretamente;
  12. Em .github/workflows/, criar arquivo Pipeline e2e.yml:
    
    name: Pipeline E2E
    
    on:
        push:
            branches:
                - main
        pull_request:
    
    jobs:
        testes-e2e:
            runs-on: ubuntu-latest
            services:
                postgres:
                    image: postgres:15
                    env:
                        POSTGRES_USER: postgres
                        POSTGRES_PASSWORD: postgres
                        POSTGRES_DB: test_db
                    ports:
                        - 5432:5432
                    options: >-
                        --health-cmd pg_isready
                        --health-interval 10s
                        --health-timeout 5s
                        --health-retries 5
    
            steps:
                - name: Fazer checkout do código
                  uses: actions/checkout@v3
    
                - name: Configurar Python 3.13
                  uses: actions/setup-python@v4
                  with:
                      python-version: "3.13"
    
                - name: Configurar Node.js 22
                  uses: actions/setup-node@v3
                  with:
                      node-version: "22"
    
                - name: Instalar dependências do backend
                  working-directory: ./paris_cafe
                  run: |
                      python -m pip install --upgrade pip
                      pip install -r requirements.txt
    
                - name: Executar migrações Django
                  working-directory: ./paris_cafe
                  env:
                      DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                      DJANGO_SETTINGS_MODULE: paris_cafe.settings
                  run: |
                      python manage.py migrate
    
                - name: Instalar dependências frontend
                  working-directory: ./cafe-frontend
                  run: npm install
    
                - name: Subir backend em background
                  working-directory: ./paris_cafe
                  env:
                      DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                      DJANGO_SETTINGS_MODULE: paris_cafe.settings
                  run: |
                      nohup python manage.py runserver 0.0.0.0:8000 &
                
                - name: Subir frontend em background
                  working-directory: ./cafe-frontend
                  run: |
                      nohup npm run dev &
    
                - name: Executar testes E2E com Cypress
                  working-directory: ./cafe-frontend
                  env:
                      VITE_BACKEND_URL: http://127.0.0.1:8000
                  run: npx cypress run
    
  13. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Pipeline e2e criada"
    • git push
    • Conferir: git status
  14. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-e2e" foi criada e suas etapas executadas corretamente.

Elaborado por Mateus Schwede
ubsocial.github.io