Ejercicios y taller de calidad
En este taller recibirás un pequeño proyecto que «funciona en mi máquina», pero no ofrece una forma fiable de comprobarlo. Lo convertiremos paso a paso en un proyecto reproducible.
Usaremos un gestor de inventario local. Cada ejercicio incluye situación, objetivo, criterio verificable y solución. Intenta resolverlo antes de comparar.
Punto de partida
import java.util.HashMap;
import java.util.Map;
public class Inventario {
private final Map<String, Integer> unidades = new HashMap<>();
public void reponer(String referencia, int cantidad) {
unidades.put(referencia, unidades.getOrDefault(referencia, 0) + cantidad);
}
public int consultar(String referencia) {
return unidades.getOrDefault(referencia, 0);
}
public void retirar(String referencia, int cantidad) {
unidades.put(referencia, consultar(referencia) - cantidad);
}
}
No hay build ni pruebas y es posible obtener existencias negativas.
1. Crear una estructura reconocible
Situación: los archivos sueltos obligan a explicar cómo compilar. Objetivo: adoptar la estructura estándar de Maven y un paquete propio. Criterio: mvn compile compila Inventario.java.
Solución
inventario/
├── pom.xml
└── src/
├── main/java/es/skilly/inventario/Inventario.java
└── test/java/es/skilly/inventario/InventarioTest.java
Ambas clases declaran package es.skilly.inventario;. La ubicación refleja el paquete y separa producción de pruebas.
2. Definir un build para Java 17
Situación: aún dependemos de comandos particulares. Objetivo: describir el proyecto y fijar Java. Criterio: mvn clean compile termina con BUILD SUCCESS.
Solución
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>es.skilly</groupId>
<artifactId>inventario</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.14.2</junit.version>
<slf4j.version>2.0.17</slf4j.version>
</properties>
</project>
El POM ya expresa coordenadas, encoding y Java 17.
3. Corregir el build
Situación: el POM declara Java 17, pero deja que Maven seleccione implícitamente la versión del compilador. Objetivo: fijar el plugin que aplica esa configuración. Criterio: mvn clean compile usa release 17 y crea clases bajo target/classes.
Solución
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</build>
Fijar el plugin evita que dos instalaciones elijan versiones implícitas diferentes. La propiedad maven.compiler.release continúa siendo la fuente de la versión objetivo.
4. Añadir JUnit con el scope correcto
Situación: no hay una comprobación automática. Objetivo: incorporar JUnit Jupiter solo para pruebas. Criterio: mvn dependency:tree muestra JUnit con scope test.
Solución
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
JUnit no forma parte de la aplicación final; por eso su scope es test. Añade Surefire junto al plugin de compilación que ya existe dentro de <build><plugins>.
5. Capturar el comportamiento válido
Situación: antes de corregir el defecto conviene proteger lo que funciona. Objetivo: probar reposición, acumulación y consulta inexistente. Criterio: tres pruebas pasan con mvn test.
Solución
package es.skilly.inventario;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InventarioTest {
private Inventario inventario;
@BeforeEach
void preparar() {
inventario = new Inventario();
}
@Test
void reponeUnidades() {
inventario.reponer("TE-01", 4);
assertEquals(4, inventario.consultar("TE-01"));
}
@Test
void acumulaReposiciones() {
inventario.reponer("TE-01", 4);
inventario.reponer("TE-01", 3);
assertEquals(7, inventario.consultar("TE-01"));
}
@Test
void unaReferenciaDesconocidaTieneCeroUnidades() {
assertEquals(0, inventario.consultar("NO-EXISTE"));
}
}
Cada prueba prepara su propio inventario, actúa y verifica un resultado observable.
6. Reproducir y corregir el defecto
Situación: retirar más unidades de las disponibles produce un valor negativo. Objetivo: crear una prueba roja y corregirlo. Criterio: la retirada imposible lanza IllegalArgumentException y conserva el estado.
Solución
@Test
void rechazaUnaRetiradaSinExistencias() {
inventario.reponer("TE-01", 2);
assertThrows(IllegalArgumentException.class,
() -> inventario.retirar("TE-01", 3));
assertEquals(2, inventario.consultar("TE-01"));
}
Añade el import estático de assertThrows y corrige el método:
public void retirar(String referencia, int cantidad) {
int disponibles = consultar(referencia);
if (cantidad <= 0 || cantidad > disponibles) {
throw new IllegalArgumentException("Cantidad no disponible");
}
unidades.put(referencia, disponibles - cantidad);
}
La validación ocurre antes de modificar el mapa.
7. Añadir logging con intención
Situación: una retirada rechazada no deja contexto. Objetivo: registrar operaciones relevantes sin sustituir las excepciones. Criterio: una retirada inválida registra referencia, cantidad y unidades disponibles.
Solución
Añade slf4j-api sin scope y slf4j-simple con scope runtime, ambos en versión ${slf4j.version}. En Inventario:
private static final Logger logger = LoggerFactory.getLogger(Inventario.class);
public void retirar(String referencia, int cantidad) {
int disponibles = consultar(referencia);
if (cantidad <= 0 || cantidad > disponibles) {
logger.warn("Retirada rechazada: referencia={}, solicitadas={}, disponibles={}",
referencia, cantidad, disponibles);
throw new IllegalArgumentException("Cantidad no disponible");
}
unidades.put(referencia, disponibles - cantidad);
logger.info("Retirada completada: referencia={}, unidades={}", referencia, cantidad);
}
Importa Logger y LoggerFactory. Los placeholders separan patrón y valores.
8. Documentar el contrato
Situación: no está claro qué ocurre ante una retirada no válida. Objetivo: documentar el comportamiento público. Criterio: mvn javadoc:javadoc genera el contrato sin errores.
Solución
/**
* Retira unidades de una referencia existente.
*
* @param referencia referencia cuyo stock se modifica
* @param cantidad número positivo de unidades que se retiran
* @throws IllegalArgumentException si la cantidad no es positiva
* o supera las unidades disponibles
*/
public void retirar(String referencia, int cantidad) {
// implementación ya corregida
}
Configura maven-javadoc-plugin 3.11.2 dentro de <plugins>. El contrato explica restricciones y error, no el algoritmo.
9. Crear un README reproducible
Situación: otra persona desconoce requisitos y comandos. Objetivo: ofrecer un recorrido desde un checkout limpio. Criterio: los comandos pueden copiarse sin instrucciones adicionales.
Solución
# Inventario
Requisitos: Java 17 y Maven 3.9 o compatible.
## Verificar
```bash
mvn clean verify
```
## Generar la documentación
```bash
mvn javadoc:javadoc
```
Los resultados se generan bajo `target/` y no se versionan.
Reto final integrado
Reconstruye el proyecto final en un directorio nuevo. Debe cumplir:
- estructura Maven y paquete
es.skilly.inventario; - compilación con Java 17;
- JUnit Jupiter 5.14.2 en scope
test; - SLF4J 2.0.17 con implementación en runtime;
- cuatro pruebas, incluida la regresión de existencias negativas;
- logging parametrizado en la retirada;
- Javadoc del contrato y README con comandos reales;
- ningún resultado de
target/versionado.
La solución es la suma de los nueve pasos. Compruébala desde la raíz:
mvn clean verify
mvn javadoc:javadoc
El primer comando debe ejecutar cuatro pruebas sin fallos y crear target/inventario-1.0.0.jar. El segundo debe crear target/reports/apidocs/index.html.
Revisión de calidad
- ¿Un fallo se reproduce mediante una prueba antes de corregirse?
- ¿Las dependencias tienen versión y scope coherentes?
- ¿Los logs aportan contexto sin duplicar la salida funcional?
- ¿Javadoc describe el contrato real?
- ¿El README solo contiene comandos ejecutados?
- ¿
mvn clean verifyfunciona sin archivos locales ocultos?
En la siguiente unidad aplicarás estas capacidades a un proyecto completo, preparado para entregarse y reproducirse desde cero.