Incluir información de CI/CD

This commit is contained in:
Isidoro Nevares 2026-03-18 17:52:49 +01:00
parent db46202c78
commit 508ca0be33
10 changed files with 327 additions and 125 deletions

View File

@ -26,7 +26,7 @@
<attribute name="optional" value="true"/> <attribute name="optional" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk-23"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-23">
<attributes> <attributes>
<attribute name="module" value="true"/> <attribute name="module" value="true"/>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
@ -37,5 +37,6 @@
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="output" path="target/classes"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View File

@ -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/<project>=UTF-8

View File

@ -1,8 +1,8 @@
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 org.eclipse.jdt.core.compiler.codegen.targetPlatform=23
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 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.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=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.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.compiler.source=23

168
README.md
View File

@ -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`. 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.
--- ---
## Conceptos básicos ## 2. Dependencias y plugins esenciales
### POM (Project Object Model) ### Testing
- **`spring-boot-starter-test`** (proyectos Spring Boot): incluye JUnit 5, AssertJ y Mockito.
Es el archivo `pom.xml`, donde se define: ### Plugins Maven importantes
* Información del proyecto (`groupId`, `artifactId`, `version`) | Plugin | Función |
* Dependencias |--------|---------|
* Plugins | `maven-compiler-plugin` | Compila el proyecto |
* Configuración de compilación | `maven-surefire-plugin` | Ejecuta tests unitarios |
| `maven-failsafe-plugin` | Ejecuta tests de integración (opcional) |
Ejemplo básico: ---
```xml
<groupId>org.ejemplo</groupId> ## 3. Tests
<artifactId>mi-proyecto</artifactId>
<version>1.0.0</version> 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
``` ```
--- ---
## Ciclo de vida de Maven (Build Lifecycle) ## 4. Flujo básico con GitHub Actions
El ciclo de vida principal está formado por fases que se ejecutan en orden: Archivo de configuración: `.github/workflows/maven.yml`
| Fase | Descripción | ```yaml
| -------- | ------------------------------------------- | name: CI
| 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 |
Ejemplo de ejecución: on:
```bash push:
mvn clean install 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 > Si algún test falla, GitHub Actions marcará el workflow como **Failed**
Maven descarga las dependencias desde repositorios.
Tipos:
* **Local** → en el ordenador (`~/.m2`)
* **Central** → repositorio público de Maven
* **Remoto** → repositorios privados (Nexus, Artifactory)
--- ---
## Dependencias ## 5. Buenas prácticas
Las librerías necesarias para el proyecto se declaran en el `pom.xml`. 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.
Ejemplo: 3. **Usa perfiles Maven** si necesitas configuraciones distintas para CI (p. ej., base de datos H2 para tests).
```xml 4. **Verifica la versión de Java**: el runner de GitHub Actions debe usar la misma que la configurada en el proyecto.
<dependency> 5. **Habilita la caché de Maven**: reduce significativamente el tiempo de build en cada ejecución.
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
```
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)

10
pom.xml
View File

@ -16,6 +16,7 @@
<properties> <properties>
<java.version>23</java.version> <java.version>23</java.version>
<junit.version>6.0.3</junit.version>
</properties> </properties>
@ -25,11 +26,20 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<!-- Starter test: incluye JUnit 5, Mockito, AssertJ, etc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -6,9 +6,9 @@ import org.lapaloma.mapamundi.vo.Continente;
public interface IContinenteDAO { public interface IContinenteDAO {
public Continente obtenerContinentePorClave(String codigo) ; public Continente obtenerContinentePorClave(String codigo) ;
public Continente actualizarContinente(Continente coche) ; public void actualizarContinente(Continente continente) ;
public Continente crearContinente(Continente coche); public void crearContinente(Continente continente);
public void borrarContinente(Continente coche); public void borrarContinente(Continente continente);
public List<Continente> obtenerListaContinentes(); public List<Continente> obtenerListaContinentes();
public List<Continente> obtenerContinentePorNombre(String nombre); public List<Continente> obtenerContinentePorNombre(String nombre);
} }

View File

@ -42,7 +42,7 @@ public class ContinenteDaoJDBC implements IContinenteDAO {
} }
@Override @Override
public Continente actualizarContinente(Continente continente) { public void actualizarContinente(Continente continente) {
String sentenciaSQL = """ String sentenciaSQL = """
UPDATE T_CONTINENTE UPDATE T_CONTINENTE
@ -64,11 +64,10 @@ public class ContinenteDaoJDBC implements IContinenteDAO {
e.printStackTrace(); e.printStackTrace();
} }
return continente;
} }
@Override @Override
public Continente crearContinente(Continente continente) { public void crearContinente(Continente continente) {
String sentenciaSQL = """ String sentenciaSQL = """
INSERT INTO T_CONTINENTE (codigo, nombre_continente) INSERT INTO T_CONTINENTE (codigo, nombre_continente)
@ -88,8 +87,6 @@ public class ContinenteDaoJDBC implements IContinenteDAO {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
return continente;
} }
@Override @Override

View File

@ -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);
}
}

View File

@ -7,36 +7,60 @@ import java.util.List;
import org.lapaloma.mapamundi.dao.IContinenteDAO; import org.lapaloma.mapamundi.dao.IContinenteDAO;
import org.lapaloma.mapamundi.dao.impl.ContinenteDaoJDBC; import org.lapaloma.mapamundi.dao.impl.ContinenteDaoJDBC;
import org.lapaloma.mapamundi.excepcion.ContinenteNoEncontradoException;
import org.lapaloma.mapamundi.vo.Continente; import org.lapaloma.mapamundi.vo.Continente;
/**
* Isidoro Nevares Martín - Virgen de la Paloma Fecha creación: 13 mar 2026
*/
public class ContinenteService { public class ContinenteService {
IContinenteDAO continenteDAO = new ContinenteDaoJDBC(); IContinenteDAO continenteDAO = new ContinenteDaoJDBC();
public Continente obtenerContinentePorClave(String codigo) { public Continente obtenerContinentePorClave(String codigo) {
Continente continente = null;
continente = continenteDAO.obtenerContinentePorClave(codigo);
if (codigo == null || codigo.isBlank()) {
throw new IllegalArgumentException("Código inválido");
}
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; return continente;
} }
public List<Continente> obtenerListaContinentes() { public List<Continente> obtenerListaContinentes() {
List<Continente> listaContinentes = null;
listaContinentes = continenteDAO.obtenerListaContinentes(); List<Continente> lista = continenteDAO.obtenerListaContinentes();
return listaContinentes; if (lista == null || lista.isEmpty()) {
throw new RuntimeException("No hay continentes disponibles");
}
return lista;
} }
public List<Continente> obtenerContinentePorNombre(String nombre) { public List<Continente> obtenerContinentePorNombre(String nombre) {
List<Continente> listaContinentes = null;
listaContinentes = continenteDAO.obtenerContinentePorNombre(nombre); if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("Nombre inválido");
return listaContinentes;
} }
List<Continente> lista = continenteDAO.obtenerContinentePorNombre(nombre);
if (lista == null || lista.isEmpty()) {
throw new ContinenteNoEncontradoException(
"No existen continentes con nombre: " + nombre
);
}
return lista;
}
} }

View File

@ -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<Continente> 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<Continente> 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<Continente> data = new ArrayList<>();
@Override
public Continente obtenerContinentePorClave(String codigo) {
return data.stream()
.filter(c -> c.getCodigo().equals(codigo))
.findFirst()
.orElse(null);
}
@Override
public List<Continente> obtenerListaContinentes() {
return new ArrayList<>(data);
}
@Override
public List<Continente> obtenerContinentePorNombre(String nombre) {
List<Continente> 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) {
}
}
}