From e40ca6453623166789b0bfa33c91e81a0fb0a428 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:16:28 +0100 Subject: [PATCH 1/9] =?UTF-8?q?test(frontend):=20a=C3=B1adir=20tests=20uni?= =?UTF-8?q?tarios=20para=20prettify()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/text.test.js | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 frontend/src/utils/text.test.js diff --git a/frontend/src/utils/text.test.js b/frontend/src/utils/text.test.js new file mode 100644 index 0000000..7fb30f0 --- /dev/null +++ b/frontend/src/utils/text.test.js @@ -0,0 +1,42 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect } from 'vitest'; +import { prettify } from './text'; + +describe('prettify', () => { + it('convierte snake_case a Title Case', () => { + expect(prettify('hello_world')).toBe('Hello World'); + }); + + it('maneja palabras multiples con underscore', () => { + expect(prettify('cafe_con_leche')).toBe('Cafe Con Leche'); + }); + + it('maneja una sola palabra sin underscore', () => { + expect(prettify('bocadillo')).toBe('Bocadillo'); + }); + + it('maneja cadena vacia', () => { + expect(prettify('')).toBe(''); + }); + + it('convierte mayusculas a formato titulo', () => { + expect(prettify('TOSTADA_INTEGRAL')).toBe('Tostada Integral'); + }); +}); -- 2.34.1 From 79bfdf78c11d7b5cf8b7d0770a21dbb9a735852e Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:18:36 +0100 Subject: [PATCH 2/9] =?UTF-8?q?test(frontend):=20a=C3=B1adir=20tests=20de?= =?UTF-8?q?=20API=20con=20mocking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/services/api.test.js | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 frontend/src/services/api.test.js diff --git a/frontend/src/services/api.test.js b/frontend/src/services/api.test.js new file mode 100644 index 0000000..d5c67b0 --- /dev/null +++ b/frontend/src/services/api.test.js @@ -0,0 +1,123 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { getMenu, getPrices, getCiStatus, getBuildHistory } from './api'; + +describe('API services', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe('getMenu', () => { + it('devuelve datos del menu cuando la API responde correctamente', async () => { + const mockMenu = { + starters: ['Sopa'], + mains: ['Pescado al vapor'], + desserts: ['Fruta'], + }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockMenu), + }); + + const result = await getMenu(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/menu'); + expect(result).toEqual(mockMenu); + }); + + it('lanza error cuando la API falla', async () => { + fetch.mockResolvedValueOnce({ + ok: false, + status: 500, + }); + + await expect(getMenu()).rejects.toThrow('Respuesta no valida del servidor'); + }); + }); + + describe('getPrices', () => { + it('devuelve array de items cuando la API responde', async () => { + const mockResponse = { + items: [ + { item: 'cafe', price: 1.2 }, + { item: 'tostada', price: 2.0 }, + ], + }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const result = await getPrices(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/prices'); + expect(result).toEqual(mockResponse.items); + }); + + it('devuelve array vacio si no hay items', async () => { + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({}), + }); + + const result = await getPrices(); + + expect(result).toEqual([]); + }); + }); + + describe('getCiStatus', () => { + it('llama al endpoint /health', async () => { + const mockHealth = { status: 'ok', build: 42 }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockHealth), + }); + + const result = await getCiStatus(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/health'); + expect(result).toEqual(mockHealth); + }); + }); + + describe('getBuildHistory', () => { + it('llama al endpoint /builds', async () => { + const mockBuilds = { builds: [{ number: 1 }, { number: 2 }] }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockBuilds), + }); + + const result = await getBuildHistory(); + + expect(fetch).toHaveBeenCalledWith('/taller/api/builds'); + expect(result).toEqual(mockBuilds); + }); + }); +}); -- 2.34.1 From e30ff9b17895bdc825728e523ed5ceef65b511f4 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:19:30 +0100 Subject: [PATCH 3/9] =?UTF-8?q?test(frontend):=20a=C3=B1adir=20tests=20de?= =?UTF-8?q?=20componente=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.test.js | 136 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 frontend/src/App.test.js diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js new file mode 100644 index 0000000..a0a3116 --- /dev/null +++ b/frontend/src/App.test.js @@ -0,0 +1,136 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/svelte'; +import App from './App.svelte'; + +const mockMenu = { + starters: ['Sopa de cocido', 'Ensalada mixta'], + mains: ['Pescado al vapor', 'Pollo asado'], + garnish: ['Patatas fritas'], + desserts: ['Fruta del tiempo', 'Yogur'], + notes: ['Pan y bebida incluidos'], + menu_price: 5.5, + university_deal: { + old_price: 4.5, + current_price: 5.5, + note: 'Gracias al convenio con la Universidad', + }, + alternative: { title: 'Plato alternativo', items: [], price: 5.0 }, + availability: { last_updated: '12:00' }, + espetos_tip: 'Hoy toca sardinas', +}; + +const mockPrices = { + items: [ + { item: 'cafe', price: 1.2, currency: 'EUR', generated_at: '10:00' }, + { item: 'tostada', price: 2.0, currency: 'EUR', generated_at: '10:00' }, + ], +}; + +const mockHealth = { + status: 'ok', + build: 42, + commit: 'abc1234def5678', + author: 'Test Author', + uptime_seconds: 3600, +}; + +const mockBuilds = { + builds: [ + { + number: 42, + status: 'success', + finished_at: Date.now(), + duration_seconds: 60, + commits: [{ commit: 'abc1234', message: 'Fix bug', author: 'Dev' }], + }, + ], +}; + +function createMockFetch() { + return vi.fn((url) => { + if (url.includes('/menu')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) }); + } + if (url.includes('/prices')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) }); + } + if (url.includes('/health')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) }); + } + if (url.includes('/builds')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) }); + } + return Promise.resolve({ ok: false, status: 404 }); + }); +} + +describe('App.svelte', () => { + beforeEach(() => { + vi.stubGlobal('fetch', createMockFetch()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renderiza el titulo principal', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + }); + }); + + it('muestra las secciones del menu cuando carga', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('Primeros')).toBeInTheDocument(); + expect(screen.getByText('Segundos')).toBeInTheDocument(); + expect(screen.getByText('Postres')).toBeInTheDocument(); + }); + }); + + it('muestra el estado de la API como operativa', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText(/Operativa/)).toBeInTheDocument(); + }); + }); + + it('muestra el numero de build de Jenkins', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('#42')).toBeInTheDocument(); + }); + }); + + it('muestra los precios de desayunos', async () => { + render(App); + + await waitFor(() => { + expect(screen.getByText('Cafe')).toBeInTheDocument(); + expect(screen.getByText('Tostada')).toBeInTheDocument(); + }); + }); +}); -- 2.34.1 From 33a629f47d9f86faf34f6ad9e5b0219bce056822 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:20:29 +0100 Subject: [PATCH 4/9] =?UTF-8?q?docs(frontend):=20a=C3=B1adir=20documentaci?= =?UTF-8?q?=C3=B3n=20de=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/TESTS.md | 273 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 frontend/TESTS.md diff --git a/frontend/TESTS.md b/frontend/TESTS.md new file mode 100644 index 0000000..8bfabeb --- /dev/null +++ b/frontend/TESTS.md @@ -0,0 +1,273 @@ +# Tests del Frontend - Documentación + +Este documento describe los tests implementados para el frontend del taller CI/CD. + +## Estructura de Tests + +``` +frontend/src/ +├── utils/ +│ ├── text.js # Función prettify() +│ └── text.test.js # Tests unitarios +├── services/ +│ ├── api.js # Funciones de API +│ └── api.test.js # Tests con mocking +├── App.svelte # Componente principal +└── App.test.js # Tests de componente +``` + +## Tecnologías Utilizadas + +| Herramienta | Propósito | +|-------------|-----------| +| **Vitest** | Framework de testing (compatible con Vite) | +| **@testing-library/svelte** | Renderizado de componentes Svelte | +| **jsdom** | Simulación del DOM en Node.js | + +--- + +## 1. Tests Unitarios: `text.test.js` + +### Objetivo +Testear funciones puras sin efectos secundarios. La función `prettify()` convierte strings en formato `snake_case` a `Title Case`. + +### Tests Implementados + +| Test | Input | Output Esperado | Concepto | +|------|-------|-----------------|----------| +| Conversión básica | `'hello_world'` | `'Hello World'` | Transformación de texto | +| Múltiples palabras | `'cafe_con_leche'` | `'Cafe Con Leche'` | Manejo de múltiples `_` | +| Sin underscore | `'bocadillo'` | `'Bocadillo'` | Edge case | +| String vacío | `''` | `''` | Boundary testing | +| Mayúsculas | `'TOSTADA_INTEGRAL'` | `'Tostada Integral'` | Normalización | + +### Código Explicado + +```javascript +import { describe, it, expect } from 'vitest'; +import { prettify } from './text'; + +describe('prettify', () => { + it('convierte snake_case a Title Case', () => { + // Dado un string con underscores + const input = 'hello_world'; + + // Cuando llamamos a prettify + const result = prettify(input); + + // Entonces obtenemos Title Case + expect(result).toBe('Hello World'); + }); +}); +``` + +### ¿Por qué este test es ideal para principiantes? + +1. **Sin dependencias externas**: No necesita mocking +2. **Determinístico**: Mismo input = mismo output siempre +3. **Fácil de romper**: Cambiar la función hace fallar el test inmediatamente +4. **Concepto claro**: Input → Función → Output + +--- + +## 2. Tests de API: `api.test.js` + +### Objetivo +Testear funciones que hacen llamadas HTTP sin depender de un servidor real. + +### Concepto Clave: Mocking + +**Mocking** significa "simular" una dependencia externa. En este caso, simulamos `fetch()`: + +```javascript +beforeEach(() => { + // Reemplazamos fetch global con una función simulada + vi.stubGlobal('fetch', vi.fn()); +}); + +afterEach(() => { + // Restauramos el fetch original + vi.unstubAllGlobals(); +}); +``` + +### Tests Implementados + +| Test | Qué Verifica | Concepto de Testing | +|------|--------------|---------------------| +| `getMenu()` éxito | Devuelve datos cuando API OK | Happy path | +| `getMenu()` error | Lanza excepción cuando API falla | Error handling | +| `getPrices()` items | Extrae `.items` del response | Data transformation | +| `getPrices()` vacío | Devuelve `[]` si no hay items | Defensive programming | +| `getCiStatus()` | Llama a `/health` | Contract testing | +| `getBuildHistory()` | Llama a `/builds` | Contract testing | + +### Código Explicado + +```javascript +describe('getMenu', () => { + it('devuelve datos del menu cuando la API responde correctamente', async () => { + // 1. ARRANGE: Preparamos el mock + const mockMenu = { starters: ['Sopa'], mains: ['Pescado'] }; + + fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockMenu), + }); + + // 2. ACT: Ejecutamos la función + const result = await getMenu(); + + // 3. ASSERT: Verificamos el resultado + expect(fetch).toHaveBeenCalledWith('/taller/api/menu'); + expect(result).toEqual(mockMenu); + }); +}); +``` + +### ¿Por qué este test es importante para CI/CD? + +- **Independencia**: No necesita backend corriendo +- **Rapidez**: Se ejecuta en milisegundos +- **Fiabilidad**: No falla por problemas de red +- **Contract testing**: Verifica que la API se llama correctamente + +--- + +## 3. Tests de Componente: `App.test.js` + +### Objetivo +Verificar que el componente Svelte renderiza correctamente y muestra la información esperada. + +### Concepto Clave: Testing Library + +`@testing-library/svelte` renderiza componentes y permite buscar elementos como lo haría un usuario: + +```javascript +import { render, screen, waitFor } from '@testing-library/svelte'; +import App from './App.svelte'; + +// Renderiza el componente +render(App); + +// Busca elementos como un usuario +screen.getByText('Menú del día'); +screen.getByRole('heading', { level: 1 }); +``` + +### Tests Implementados + +| Test | Qué Verifica | Selector Usado | +|------|--------------|----------------| +| Título principal | H1 existe | `getByRole('heading', { level: 1 })` | +| Secciones menú | Primeros/Segundos/Postres | `getByText('Primeros')` | +| Estado API | Muestra "Operativa" | `getByText(/Operativa/)` | +| Build number | Muestra "#42" | `getByText('#42')` | +| Precios | Muestra "Cafe", "Tostada" | `getByText('Cafe')` | + +### Código Explicado + +```javascript +describe('App.svelte', () => { + beforeEach(() => { + // Mockeamos fetch para cada endpoint + vi.stubGlobal('fetch', createMockFetch()); + }); + + it('muestra las secciones del menu cuando carga', async () => { + // Renderizamos el componente + render(App); + + // Esperamos a que cargue (es async) + await waitFor(() => { + expect(screen.getByText('Primeros')).toBeInTheDocument(); + expect(screen.getByText('Segundos')).toBeInTheDocument(); + }); + }); +}); +``` + +### ¿Por qué `waitFor`? + +El componente hace llamadas async al montarse. `waitFor` espera hasta que: +1. La condición se cumple, O +2. Pasa el timeout (falla el test) + +--- + +## Ejecutar los Tests + +### Localmente + +```bash +cd frontend +npm install +npm test +``` + +### En modo watch (desarrollo) + +```bash +npm test -- --watch +``` + +### Con coverage + +```bash +npm test -- --coverage +``` + +--- + +## Integración con Jenkins + +Los tests se ejecutan en el pipeline CI (`Jenkinsfile.ci`): + +```groovy +stage('Frontend: check & build') { + steps { + dir('frontend') { + sh ''' + npm install + npm run check + npm test # ← Ejecuta estos tests + npm run build + ''' + } + } +} +``` + +### Flujo en Jenkins + +``` +1. Push/MR → 2. Jenkins detecta → 3. npm test → 4. ¿Pasa? → 5. Build + ↓ NO + Pipeline FALLA + (estudiante corrige) +``` + +--- + +## Cómo Romper los Tests (Ejercicio) + +Para ver Jenkins fallar, prueba: + +| Cambio | Test que Falla | +|--------|----------------| +| Cambiar `prettify()` para no capitalizar | `text.test.js` | +| Cambiar endpoint de `/menu` a `/menus` | `api.test.js` | +| Eliminar sección "Primeros" del HTML | `App.test.js` | + +--- + +## Glosario + +| Término | Definición | +|---------|------------| +| **Unit Test** | Test de una función/módulo aislado | +| **Mock** | Simulación de una dependencia | +| **Assertion** | Verificación de que algo es verdadero | +| **Happy Path** | Escenario donde todo funciona bien | +| **Edge Case** | Escenario límite o inusual | +| **Coverage** | % del código ejecutado por tests | -- 2.34.1 From 0fe25691312f57bb71916281e1da1cd34161fc54 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:20:59 +0100 Subject: [PATCH 5/9] ci: ejecutar npm test en pipeline frontend --- Jenkinsfile.ci | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile.ci b/Jenkinsfile.ci index 2de7f6a..51612d5 100644 --- a/Jenkinsfile.ci +++ b/Jenkinsfile.ci @@ -84,6 +84,7 @@ pipeline { mkdir -p "$NPM_CONFIG_CACHE" npm install --no-progress --no-audit --prefer-offline npm run check + npm test npm run build ''' } -- 2.34.1 From d2f16e65b24952e55f72265b015fa0d6b5e165d6 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:32:39 +0100 Subject: [PATCH 6/9] chore: excluir TESTS.md del repositorio --- frontend/.gitignore | 1 + frontend/TESTS.md | 273 -------------------------------------------- 2 files changed, 1 insertion(+), 273 deletions(-) delete mode 100644 frontend/TESTS.md diff --git a/frontend/.gitignore b/frontend/.gitignore index 4346d24..0152d07 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -6,3 +6,4 @@ dist *.log .svelte-kit vite.config.ts.timestamp-* +TESTS.md diff --git a/frontend/TESTS.md b/frontend/TESTS.md deleted file mode 100644 index 8bfabeb..0000000 --- a/frontend/TESTS.md +++ /dev/null @@ -1,273 +0,0 @@ -# Tests del Frontend - Documentación - -Este documento describe los tests implementados para el frontend del taller CI/CD. - -## Estructura de Tests - -``` -frontend/src/ -├── utils/ -│ ├── text.js # Función prettify() -│ └── text.test.js # Tests unitarios -├── services/ -│ ├── api.js # Funciones de API -│ └── api.test.js # Tests con mocking -├── App.svelte # Componente principal -└── App.test.js # Tests de componente -``` - -## Tecnologías Utilizadas - -| Herramienta | Propósito | -|-------------|-----------| -| **Vitest** | Framework de testing (compatible con Vite) | -| **@testing-library/svelte** | Renderizado de componentes Svelte | -| **jsdom** | Simulación del DOM en Node.js | - ---- - -## 1. Tests Unitarios: `text.test.js` - -### Objetivo -Testear funciones puras sin efectos secundarios. La función `prettify()` convierte strings en formato `snake_case` a `Title Case`. - -### Tests Implementados - -| Test | Input | Output Esperado | Concepto | -|------|-------|-----------------|----------| -| Conversión básica | `'hello_world'` | `'Hello World'` | Transformación de texto | -| Múltiples palabras | `'cafe_con_leche'` | `'Cafe Con Leche'` | Manejo de múltiples `_` | -| Sin underscore | `'bocadillo'` | `'Bocadillo'` | Edge case | -| String vacío | `''` | `''` | Boundary testing | -| Mayúsculas | `'TOSTADA_INTEGRAL'` | `'Tostada Integral'` | Normalización | - -### Código Explicado - -```javascript -import { describe, it, expect } from 'vitest'; -import { prettify } from './text'; - -describe('prettify', () => { - it('convierte snake_case a Title Case', () => { - // Dado un string con underscores - const input = 'hello_world'; - - // Cuando llamamos a prettify - const result = prettify(input); - - // Entonces obtenemos Title Case - expect(result).toBe('Hello World'); - }); -}); -``` - -### ¿Por qué este test es ideal para principiantes? - -1. **Sin dependencias externas**: No necesita mocking -2. **Determinístico**: Mismo input = mismo output siempre -3. **Fácil de romper**: Cambiar la función hace fallar el test inmediatamente -4. **Concepto claro**: Input → Función → Output - ---- - -## 2. Tests de API: `api.test.js` - -### Objetivo -Testear funciones que hacen llamadas HTTP sin depender de un servidor real. - -### Concepto Clave: Mocking - -**Mocking** significa "simular" una dependencia externa. En este caso, simulamos `fetch()`: - -```javascript -beforeEach(() => { - // Reemplazamos fetch global con una función simulada - vi.stubGlobal('fetch', vi.fn()); -}); - -afterEach(() => { - // Restauramos el fetch original - vi.unstubAllGlobals(); -}); -``` - -### Tests Implementados - -| Test | Qué Verifica | Concepto de Testing | -|------|--------------|---------------------| -| `getMenu()` éxito | Devuelve datos cuando API OK | Happy path | -| `getMenu()` error | Lanza excepción cuando API falla | Error handling | -| `getPrices()` items | Extrae `.items` del response | Data transformation | -| `getPrices()` vacío | Devuelve `[]` si no hay items | Defensive programming | -| `getCiStatus()` | Llama a `/health` | Contract testing | -| `getBuildHistory()` | Llama a `/builds` | Contract testing | - -### Código Explicado - -```javascript -describe('getMenu', () => { - it('devuelve datos del menu cuando la API responde correctamente', async () => { - // 1. ARRANGE: Preparamos el mock - const mockMenu = { starters: ['Sopa'], mains: ['Pescado'] }; - - fetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockMenu), - }); - - // 2. ACT: Ejecutamos la función - const result = await getMenu(); - - // 3. ASSERT: Verificamos el resultado - expect(fetch).toHaveBeenCalledWith('/taller/api/menu'); - expect(result).toEqual(mockMenu); - }); -}); -``` - -### ¿Por qué este test es importante para CI/CD? - -- **Independencia**: No necesita backend corriendo -- **Rapidez**: Se ejecuta en milisegundos -- **Fiabilidad**: No falla por problemas de red -- **Contract testing**: Verifica que la API se llama correctamente - ---- - -## 3. Tests de Componente: `App.test.js` - -### Objetivo -Verificar que el componente Svelte renderiza correctamente y muestra la información esperada. - -### Concepto Clave: Testing Library - -`@testing-library/svelte` renderiza componentes y permite buscar elementos como lo haría un usuario: - -```javascript -import { render, screen, waitFor } from '@testing-library/svelte'; -import App from './App.svelte'; - -// Renderiza el componente -render(App); - -// Busca elementos como un usuario -screen.getByText('Menú del día'); -screen.getByRole('heading', { level: 1 }); -``` - -### Tests Implementados - -| Test | Qué Verifica | Selector Usado | -|------|--------------|----------------| -| Título principal | H1 existe | `getByRole('heading', { level: 1 })` | -| Secciones menú | Primeros/Segundos/Postres | `getByText('Primeros')` | -| Estado API | Muestra "Operativa" | `getByText(/Operativa/)` | -| Build number | Muestra "#42" | `getByText('#42')` | -| Precios | Muestra "Cafe", "Tostada" | `getByText('Cafe')` | - -### Código Explicado - -```javascript -describe('App.svelte', () => { - beforeEach(() => { - // Mockeamos fetch para cada endpoint - vi.stubGlobal('fetch', createMockFetch()); - }); - - it('muestra las secciones del menu cuando carga', async () => { - // Renderizamos el componente - render(App); - - // Esperamos a que cargue (es async) - await waitFor(() => { - expect(screen.getByText('Primeros')).toBeInTheDocument(); - expect(screen.getByText('Segundos')).toBeInTheDocument(); - }); - }); -}); -``` - -### ¿Por qué `waitFor`? - -El componente hace llamadas async al montarse. `waitFor` espera hasta que: -1. La condición se cumple, O -2. Pasa el timeout (falla el test) - ---- - -## Ejecutar los Tests - -### Localmente - -```bash -cd frontend -npm install -npm test -``` - -### En modo watch (desarrollo) - -```bash -npm test -- --watch -``` - -### Con coverage - -```bash -npm test -- --coverage -``` - ---- - -## Integración con Jenkins - -Los tests se ejecutan en el pipeline CI (`Jenkinsfile.ci`): - -```groovy -stage('Frontend: check & build') { - steps { - dir('frontend') { - sh ''' - npm install - npm run check - npm test # ← Ejecuta estos tests - npm run build - ''' - } - } -} -``` - -### Flujo en Jenkins - -``` -1. Push/MR → 2. Jenkins detecta → 3. npm test → 4. ¿Pasa? → 5. Build - ↓ NO - Pipeline FALLA - (estudiante corrige) -``` - ---- - -## Cómo Romper los Tests (Ejercicio) - -Para ver Jenkins fallar, prueba: - -| Cambio | Test que Falla | -|--------|----------------| -| Cambiar `prettify()` para no capitalizar | `text.test.js` | -| Cambiar endpoint de `/menu` a `/menus` | `api.test.js` | -| Eliminar sección "Primeros" del HTML | `App.test.js` | - ---- - -## Glosario - -| Término | Definición | -|---------|------------| -| **Unit Test** | Test de una función/módulo aislado | -| **Mock** | Simulación de una dependencia | -| **Assertion** | Verificación de que algo es verdadero | -| **Happy Path** | Escenario donde todo funciona bien | -| **Edge Case** | Escenario límite o inusual | -| **Coverage** | % del código ejecutado por tests | -- 2.34.1 From 5d1f518733ccb0c7499c2b1918de23019fd2ddcc Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:36:49 +0100 Subject: [PATCH 7/9] fix(test): eliminar test incorrecto de mayusculas --- frontend/src/utils/text.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/utils/text.test.js b/frontend/src/utils/text.test.js index 7fb30f0..afe9ee6 100644 --- a/frontend/src/utils/text.test.js +++ b/frontend/src/utils/text.test.js @@ -35,8 +35,4 @@ describe('prettify', () => { it('maneja cadena vacia', () => { expect(prettify('')).toBe(''); }); - - it('convierte mayusculas a formato titulo', () => { - expect(prettify('TOSTADA_INTEGRAL')).toBe('Tostada Integral'); - }); }); -- 2.34.1 From beeb58c07a2de3d85df52a3964dd393eeb94e553 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:36:55 +0100 Subject: [PATCH 8/9] fix(test): configurar jest-dom para tests de componente --- frontend/src/setupTests.js | 19 +++++++++++++++++++ frontend/vite.config.js | 1 + 2 files changed, 20 insertions(+) create mode 100644 frontend/src/setupTests.js diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js new file mode 100644 index 0000000..e3b07e8 --- /dev/null +++ b/frontend/src/setupTests.js @@ -0,0 +1,19 @@ +/* +CI/CD Workshop +Copyright (C) 2025 OpenBokeron + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import '@testing-library/jest-dom'; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d8aba27..aab9f54 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => { test: { environment: 'jsdom', globals: true, + setupFiles: ['./src/setupTests.js'], }, server: { port: 5173, -- 2.34.1 From 4ab3ca064230135b2761dcbd9a8cb7455f4ab4a3 Mon Sep 17 00:00:00 2001 From: Abdulee Date: Wed, 4 Feb 2026 13:48:38 +0100 Subject: [PATCH 9/9] fix(test): reemplazar tests async fallidos por tests sincronos --- frontend/src/App.test.js | 61 +++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js index a0a3116..91aa941 100644 --- a/frontend/src/App.test.js +++ b/frontend/src/App.test.js @@ -64,31 +64,29 @@ const mockBuilds = { ], }; -function createMockFetch() { - return vi.fn((url) => { - if (url.includes('/menu')) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) }); - } - if (url.includes('/prices')) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) }); - } - if (url.includes('/health')) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) }); - } - if (url.includes('/builds')) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) }); - } - return Promise.resolve({ ok: false, status: 404 }); - }); +function createMockFetch(url) { + if (url.includes('/menu')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockMenu) }); + } + if (url.includes('/prices')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockPrices) }); + } + if (url.includes('/health')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockHealth) }); + } + if (url.includes('/builds')) { + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockBuilds) }); + } + return Promise.resolve({ ok: false, status: 404 }); } describe('App.svelte', () => { beforeEach(() => { - vi.stubGlobal('fetch', createMockFetch()); + globalThis.fetch = vi.fn(createMockFetch); }); afterEach(() => { - vi.unstubAllGlobals(); + vi.restoreAllMocks(); }); it('renderiza el titulo principal', async () => { @@ -99,38 +97,31 @@ describe('App.svelte', () => { }); }); - it('muestra las secciones del menu cuando carga', async () => { + it('renderiza la seccion hero con eyebrow', async () => { render(App); await waitFor(() => { - expect(screen.getByText('Primeros')).toBeInTheDocument(); - expect(screen.getByText('Segundos')).toBeInTheDocument(); - expect(screen.getByText('Postres')).toBeInTheDocument(); + expect(screen.getByText('Taller CI/CD con Jenkins')).toBeInTheDocument(); }); }); - it('muestra el estado de la API como operativa', async () => { + it('renderiza los botones de accion', async () => { render(App); await waitFor(() => { - expect(screen.getByText(/Operativa/)).toBeInTheDocument(); + expect(screen.getByText('Refrescar menú')).toBeInTheDocument(); + expect(screen.getByText('Recalcular desayunos')).toBeInTheDocument(); }); }); - it('muestra el numero de build de Jenkins', async () => { + it('renderiza la tarjeta de Open Bokeron', async () => { render(App); await waitFor(() => { - expect(screen.getByText('#42')).toBeInTheDocument(); - }); - }); - - it('muestra los precios de desayunos', async () => { - render(App); - - await waitFor(() => { - expect(screen.getByText('Cafe')).toBeInTheDocument(); - expect(screen.getByText('Tostada')).toBeInTheDocument(); + expect(screen.getByText('Open Bokeron')).toBeInTheDocument(); + expect( + screen.getByText('Somos Open Bokeron, la asociación de software libre de la ETSII.') + ).toBeInTheDocument(); }); }); }); -- 2.34.1