Dependencias con Maven
Nuestro proyecto ya se construye con Maven. Ahora queremos utilizar JUnit, una biblioteca que no forma parte del JDK. Copiar un JAR a lib/ resolvería solo nuestra máquina: habría que recordar de dónde salió, su versión y sus propias dependencias.
Maven permite declarar lo que necesita el proyecto. A partir de esa declaración, otra persona puede obtener las mismas bibliotecas al ejecutar el build.
Coordenadas y Maven Central
Cada dependencia se identifica mediante coordenadas:
groupId: organización que publica;artifactId: módulo concreto;version: versión exacta.
Maven busca por defecto en Maven Central. La primera ejecución descarga los artefactos y sus POM; después reutiliza el repositorio local de ~/.m2/repository. Esa caché mejora la velocidad, pero no sustituye a declarar la dependencia en pom.xml.
Usaremos JUnit Jupiter 5.14.2, una versión estable comprobada con Java 17:
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.14.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
La versión está fijada: no usamos LATEST, RELEASE ni rangos abiertos. Así el build no selecciona silenciosamente otra versión mañana.
Qué ocurre al resolver
junit-jupiter es una dependencia directa porque aparece en nuestro POM. Esta depende a su vez de otros artefactos, que son dependencias transitivas. Maven lee sus POM y construye el grafo completo.
Podemos inspeccionarlo con:
mvn dependency:tree
Una salida resumida contiene una jerarquía similar a:
es.skilly:gestor-tareas:jar:1.0.0
\- org.junit.jupiter:junit-jupiter:jar:5.14.2:test
+- org.junit.jupiter:junit-jupiter-api:jar:5.14.2:test
+- org.junit.jupiter:junit-jupiter-params:jar:5.14.2:test
\- org.junit.jupiter:junit-jupiter-engine:jar:5.14.2:test
No copiamos esos JARs ni escribimos a mano cada dependencia transitiva. Sí debemos revisar el árbol cuando una biblioteca incorpora algo inesperado o dos caminos solicitan versiones diferentes. Maven media el conflicto, pero que el build termine no garantiza por sí solo que la versión elegida sea la adecuada.
Scopes esenciales
El scope indica cuándo necesita el proyecto una dependencia:
compile: disponible para compilar, probar y ejecutar producción; es el scope predeterminado;test: disponible al compilar y ejecutar tests, pero no en el classpath normal de producción;runtime: no hace falta para compilar producción, pero sí al ejecutarla.
provided expresa que el entorno proporcionará la dependencia al ejecutar. Existe, pero no lo necesitamos en este curso.
JUnit usa test porque la aplicación no debe necesitar JUnit para funcionar. Este código en src/main/java sería incorrecto:
import org.junit.jupiter.api.Test; // No está en el classpath de producción.
En cambio, los imports de JUnit sí pertenecen a src/test/java.
POM reproducible del ejemplo
<?xml version="1.0" encoding="UTF-8"?>
<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>gestor-tareas</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>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>
Surefire es el plugin que ejecutará los tests. Fijamos su versión para no depender de un valor implícito antiguo. Todavía no diseñaremos tests; aquí solo comprobamos que la biblioteca llega al ámbito correcto:
mvn dependency:tree
mvn test
Sin clases de test, mvn test debe finalizar correctamente indicando que no hay tests que ejecutar.
Primera descarga y modo local
En la primera ejecución veremos descargas. En las siguientes, Maven puede usar ~/.m2/repository/org/junit/.... No debemos versionar esa carpeta ni asumir que ya existe en otro equipo.
Una prueba desde una caché vacía obliga a descargar; una prueba posterior puede reutilizarla. En ambos casos, el POM es la fuente de verdad.
Mini reto
Este fragmento tiene tres problemas:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>LATEST</version>
</dependency>
Corrígelo y explica el efecto sobre producción.
Solución
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
La versión fija hace repetible la resolución; test impide que JUnit forme parte del classpath normal de producción. Además, la dependencia debe estar dentro de <dependencies>, no como un JAR copiado al repositorio.
Ya sabemos cómo llega JUnit al proyecto. En la siguiente unidad aprenderemos a utilizarlo para comprobar comportamiento real.