diff --git a/.classpath b/.classpath index 8e75d71..b144583 100644 --- a/.classpath +++ b/.classpath @@ -26,7 +26,7 @@ - + @@ -37,5 +37,6 @@ + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..29abf99 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 79a4c99..105f116 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,8 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=23 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.compliance=23 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate @@ -10,5 +10,5 @@ org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=23 diff --git a/README.md b/README.md index 75ebf19..f35a476 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,101 @@ -# Maven – Resumen rápido +# CI/CD con Maven y GitHub Actions -## ¿Qué es Maven? +## 1. Estructura del proyecto Maven -**Apache Maven** es una herramienta de gestión y automatización de proyectos Java. +Un proyecto Maven bien organizado es la base para que el pipeline de CI funcione sin problemas: -Permite compilar, probar, empaquetar y gestionar dependencias de forma automática. -Todo se configura mediante un archivo central llamado `pom.xml`. - ---- - -## Conceptos básicos - -### POM (Project Object Model) - -Es el archivo `pom.xml`, donde se define: - -* Información del proyecto (`groupId`, `artifactId`, `version`) -* Dependencias -* Plugins -* Configuración de compilación - -Ejemplo básico: -```xml -org.ejemplo -mi-proyecto -1.0.0 +``` +proyecto/ +├── src/ +│ ├── main/java/ # Código de producción +│ └── test/java/ # Tests unitarios +└── pom.xml # Dependencias, plugins y versión de Java ``` +> **Regla clave:** Si `mvn clean install` funciona en local → debe funcionar en CI. + --- -## Ciclo de vida de Maven (Build Lifecycle) +## 2. Dependencias y plugins esenciales -El ciclo de vida principal está formado por fases que se ejecutan en orden: +### Testing +- **`spring-boot-starter-test`** (proyectos Spring Boot): incluye JUnit 5, AssertJ y Mockito. -| Fase | Descripción | -| -------- | ------------------------------------------- | -| validate | Comprueba que el proyecto es correcto | -| compile | Compila el código fuente | -| test | Ejecuta los tests | -| package | Empaqueta la aplicación (JAR/WAR) | -| verify | Verifica que el paquete es válido | -| install | Instala el paquete en el repositorio local | -| deploy | Publica el paquete en un repositorio remoto | +### Plugins Maven importantes -Ejemplo de ejecución: -```bash -mvn clean install +| Plugin | Función | +|--------|---------| +| `maven-compiler-plugin` | Compila el proyecto | +| `maven-surefire-plugin` | Ejecuta tests unitarios | +| `maven-failsafe-plugin` | Ejecuta tests de integración (opcional) | + +--- + +## 3. Tests + +Buenas prácticas para que los tests pasen en un entorno limpio de CI: + +- Deben ejecutarse **sin depender del IDE**. +- Evitar dependencias de rutas locales o bases de datos externas → usar **H2 en memoria** si es necesario. +- Nombres descriptivos con un único escenario por test: + ``` + metodo_cuandoCondicion_retornaResultado + ``` + +--- + +## 4. Flujo básico con GitHub Actions + +Archivo de configuración: `.github/workflows/maven.yml` + +```yaml +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '23' + distribution: 'temurin' + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build with Maven + run: mvn clean install ``` ---- +### ¿Qué hace `mvn clean install`? +1. **clean** → Elimina artefactos anteriores +2. **compile** → Compila el código fuente +3. **test** → Ejecuta los tests unitarios +4. **package** → Genera el `.jar` o `.war` -## Repositorios - -Maven descarga las dependencias desde repositorios. - -Tipos: - -* **Local** → en el ordenador (`~/.m2`) -* **Central** → repositorio público de Maven -* **Remoto** → repositorios privados (Nexus, Artifactory) +> Si algún test falla, GitHub Actions marcará el workflow como **Failed** ✗ --- -## Dependencias +## 5. Buenas prácticas -Las librerías necesarias para el proyecto se declaran en el `pom.xml`. - -Ejemplo: -```xml - - mysql - mysql-connector-java - 8.0.33 - -``` - -Maven descargará automáticamente la librería. - ---- - -## Comandos Maven más usados - -| Comando | Función | -| ------------- | -------------------------------------------- | -| `mvn compile` | Compila el proyecto | -| `mvn test` | Ejecuta los tests | -| `mvn package` | Genera el JAR/WAR | -| `mvn install` | Instala el artefacto en el repositorio local | -| `mvn clean` | Elimina la carpeta `target` | - ---- - -## Estructura típica de un proyecto Maven -``` -proyecto -│ -├─ pom.xml -└─ src - ├─ main - │ ├─ java - │ └─ resources - └─ test - └─ java -``` - ---- - -## Idea clave - -Maven sigue el principio **"Convention over Configuration"**: -si respetas su estructura estándar de proyecto, **necesitas muy poca configuración**. - ---- -Fuentes: [ChatGPT](https://chat.openai.com) + [Claude](https://claude.ai) \ No newline at end of file +1. **Mantén el `pom.xml` limpio**: no sobreescribas versiones de dependencias gestionadas por Spring Boot. +2. **Evita dependencias del entorno local**: sin rutas absolutas ni servicios externos sin mock. +3. **Usa perfiles Maven** si necesitas configuraciones distintas para CI (p. ej., base de datos H2 para tests). +4. **Verifica la versión de Java**: el runner de GitHub Actions debe usar la misma que la configurada en el proyecto. +5. **Habilita la caché de Maven**: reduce significativamente el tiempo de build en cada ejecución. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 89169a8..ab6c06d 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ 23 + 6.0.3 @@ -25,11 +26,20 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + com.mysql mysql-connector-j runtime + diff --git a/src/main/java/org/lapaloma/mapamundi/dao/IContinenteDAO.java b/src/main/java/org/lapaloma/mapamundi/dao/IContinenteDAO.java index 0e9ddf7..228e87a 100644 --- a/src/main/java/org/lapaloma/mapamundi/dao/IContinenteDAO.java +++ b/src/main/java/org/lapaloma/mapamundi/dao/IContinenteDAO.java @@ -6,9 +6,9 @@ import org.lapaloma.mapamundi.vo.Continente; public interface IContinenteDAO { public Continente obtenerContinentePorClave(String codigo) ; - public Continente actualizarContinente(Continente coche) ; - public Continente crearContinente(Continente coche); - public void borrarContinente(Continente coche); + public void actualizarContinente(Continente continente) ; + public void crearContinente(Continente continente); + public void borrarContinente(Continente continente); public List obtenerListaContinentes(); public List obtenerContinentePorNombre(String nombre); } diff --git a/src/main/java/org/lapaloma/mapamundi/dao/impl/ContinenteDaoJDBC.java b/src/main/java/org/lapaloma/mapamundi/dao/impl/ContinenteDaoJDBC.java index abbc96e..3e6114b 100644 --- a/src/main/java/org/lapaloma/mapamundi/dao/impl/ContinenteDaoJDBC.java +++ b/src/main/java/org/lapaloma/mapamundi/dao/impl/ContinenteDaoJDBC.java @@ -42,7 +42,7 @@ public class ContinenteDaoJDBC implements IContinenteDAO { } @Override - public Continente actualizarContinente(Continente continente) { + public void actualizarContinente(Continente continente) { String sentenciaSQL = """ UPDATE T_CONTINENTE @@ -64,11 +64,10 @@ public class ContinenteDaoJDBC implements IContinenteDAO { e.printStackTrace(); } - return continente; } @Override - public Continente crearContinente(Continente continente) { + public void crearContinente(Continente continente) { String sentenciaSQL = """ INSERT INTO T_CONTINENTE (codigo, nombre_continente) @@ -88,8 +87,6 @@ public class ContinenteDaoJDBC implements IContinenteDAO { } catch (Exception e) { e.printStackTrace(); } - - return continente; } @Override diff --git a/src/main/java/org/lapaloma/mapamundi/excepcion/ContinenteNoEncontradoException.java b/src/main/java/org/lapaloma/mapamundi/excepcion/ContinenteNoEncontradoException.java new file mode 100644 index 0000000..56853fb --- /dev/null +++ b/src/main/java/org/lapaloma/mapamundi/excepcion/ContinenteNoEncontradoException.java @@ -0,0 +1,12 @@ +package org.lapaloma.mapamundi.excepcion; + +public class ContinenteNoEncontradoException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = -3344627619585104664L; + + public ContinenteNoEncontradoException(String mensaje) { + super(mensaje); + } +} diff --git a/src/main/java/org/lapaloma/mapamundi/service/ContinenteService.java b/src/main/java/org/lapaloma/mapamundi/service/ContinenteService.java index 19f9961..7305839 100644 --- a/src/main/java/org/lapaloma/mapamundi/service/ContinenteService.java +++ b/src/main/java/org/lapaloma/mapamundi/service/ContinenteService.java @@ -7,36 +7,60 @@ import java.util.List; import org.lapaloma.mapamundi.dao.IContinenteDAO; import org.lapaloma.mapamundi.dao.impl.ContinenteDaoJDBC; +import org.lapaloma.mapamundi.excepcion.ContinenteNoEncontradoException; import org.lapaloma.mapamundi.vo.Continente; -/** - * Isidoro Nevares Martín - Virgen de la Paloma Fecha creación: 13 mar 2026 - */ public class ContinenteService { + IContinenteDAO continenteDAO = new ContinenteDaoJDBC(); public Continente obtenerContinentePorClave(String codigo) { - Continente continente = null; + + + if (codigo == null || codigo.isBlank()) { + throw new IllegalArgumentException("Código inválido"); + } - continente = continenteDAO.obtenerContinentePorClave(codigo); + Continente continente = continenteDAO.obtenerContinentePorClave(codigo); + // Esta línea simula un error de negocio, ignorando lo que devuelve el DAO + continente = null; + + + if (continente == null) { + throw new ContinenteNoEncontradoException( + "No existe el continente con código: " + codigo + ); + } + return continente; } public List obtenerListaContinentes() { - List listaContinentes = null; - listaContinentes = continenteDAO.obtenerListaContinentes(); + List lista = continenteDAO.obtenerListaContinentes(); - return listaContinentes; + if (lista == null || lista.isEmpty()) { + throw new RuntimeException("No hay continentes disponibles"); + } + + return lista; } public List obtenerContinentePorNombre(String nombre) { - List listaContinentes = null; - listaContinentes = continenteDAO.obtenerContinentePorNombre(nombre); + if (nombre == null || nombre.isBlank()) { + throw new IllegalArgumentException("Nombre inválido"); + } - return listaContinentes; + List lista = continenteDAO.obtenerContinentePorNombre(nombre); + + if (lista == null || lista.isEmpty()) { + throw new ContinenteNoEncontradoException( + "No existen continentes con nombre: " + nombre + ); + } + + return lista; } - -} +} \ No newline at end of file diff --git a/src/test/java/org/lapaloma/mapamundi/service/ContinenteServiceTest.java b/src/test/java/org/lapaloma/mapamundi/service/ContinenteServiceTest.java new file mode 100644 index 0000000..b6d9ea5 --- /dev/null +++ b/src/test/java/org/lapaloma/mapamundi/service/ContinenteServiceTest.java @@ -0,0 +1,166 @@ +package org.lapaloma.mapamundi.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lapaloma.mapamundi.dao.IContinenteDAO; +import org.lapaloma.mapamundi.excepcion.ContinenteNoEncontradoException; +import org.lapaloma.mapamundi.vo.Continente; + +class ContinenteServiceTest { + + private ContinenteService continenteService; + private FakeContinenteDAO fakeDAO; + + @BeforeEach + void setUp() { + fakeDAO = new FakeContinenteDAO(); + continenteService = new ContinenteService(); + continenteService.continenteDAO = fakeDAO; + } + + // ========================= + // obtenerContinentePorClave + // ========================= + + @Test + void obtenerContinentePorClave_cuandoCodigoEsNull_lanzaExcepcion() { + assertThrows(IllegalArgumentException.class, () -> { + continenteService.obtenerContinentePorClave(null); + }); + } + + @Test + void obtenerContinentePorClave_cuandoCodigoEstaVacio_lanzaExcepcion() { + assertThrows(IllegalArgumentException.class, () -> { + continenteService.obtenerContinentePorClave(""); + }); + } + + @Test + void obtenerContinentePorClave_cuandoNoExiste_lanzaExcepcion() { + assertThrows(ContinenteNoEncontradoException.class, () -> { + continenteService.obtenerContinentePorClave("XX"); + }); + } + + @Test + void obtenerContinentePorClave_cuandoExiste_retornaContinente() { + fakeDAO.crearContinente(new Continente("EU", "Europa")); + + Continente resultado = continenteService.obtenerContinentePorClave("EU"); + + assertNotNull(resultado); + assertEquals("Europa", resultado.getNombre()); + } + + // ========================= + // obtenerListaContinentes + // ========================= + + @Test + void obtenerListaContinentes_cuandoListaEstaVacia_lanzaExcepcion() { + assertThrows(RuntimeException.class, () -> { + continenteService.obtenerListaContinentes(); + }); + } + + @Test + void obtenerListaContinentes_cuandoHayDatos_retornaLista() { + fakeDAO.crearContinente(new Continente("EU", "Europa")); + + List resultado = continenteService.obtenerListaContinentes(); + + assertNotNull(resultado); + assertEquals(1, resultado.size()); + } + + // ========================= + // obtenerContinentePorNombre + // ========================= + + @Test + void obtenerContinentePorNombre_cuandoNombreEsNull_lanzaExcepcion() { + assertThrows(IllegalArgumentException.class, () -> { + continenteService.obtenerContinentePorNombre(null); + }); + } + + @Test + void obtenerContinentePorNombre_cuandoNombreEstaVacio_lanzaExcepcion() { + assertThrows(IllegalArgumentException.class, () -> { + continenteService.obtenerContinentePorNombre(""); + }); + } + + @Test + void obtenerContinentePorNombre_cuandoNoExiste_lanzaExcepcion() { + assertThrows(ContinenteNoEncontradoException.class, () -> { + continenteService.obtenerContinentePorNombre("Inexistente"); + }); + } + + @Test + void obtenerContinentePorNombre_cuandoExiste_retornaLista() { + fakeDAO.crearContinente(new Continente("EU", "Europa")); + + List resultado = continenteService.obtenerContinentePorNombre("Europa"); + + assertEquals(1, resultado.size()); + } + + // ========================= + // Fake DAO. Se crea el DAO dentro del test para no depender de la conexión a la base de datos, de si hay red, de si accede a un fichero... + // En caso de usar el DOA real (ContinenteDaoJDBC) estaríamos hablando de prubeas de integración. + // ========================= + + static class FakeContinenteDAO implements IContinenteDAO { + + private List data = new ArrayList<>(); + + @Override + public Continente obtenerContinentePorClave(String codigo) { + return data.stream() + .filter(c -> c.getCodigo().equals(codigo)) + .findFirst() + .orElse(null); + } + + @Override + public List obtenerListaContinentes() { + return new ArrayList<>(data); + } + + @Override + public List obtenerContinentePorNombre(String nombre) { + List resultado = new ArrayList<>(); + + for (Continente c : data) { + if (c.getNombre().equals(nombre)) { + resultado.add(c); + } + } + return resultado; + } + + @Override + public void actualizarContinente(Continente continente) { + } + + @Override + public void crearContinente(Continente continente) { + data.add(continente); + } + + @Override + public void borrarContinente(Continente continente) { + + } + } +} \ No newline at end of file