Il progetto che funziona solo sul nostro computer
Fin qui abbiamo compilato molti esempi con una riga di javac. È un buon modo per vedere dove finisce il sorgente e dove comincia il programma eseguibile. Quando i file diventano dieci, arrivano risorse e test, e un collega deve ricostruire lo stesso risultato, quella riga non racconta più tutto. Dove mettiamo i test? Quale versione di Java usiamo? Come otteniamo JUnit? Che cosa contiene il JAR che consegniamo? Se la risposta è «sul mio computer funziona», il progetto non è ancora trasmissibile.
Costruiamo un piccolo catalogo con un titolo per riga. L'applicazione legge un file UTF-8, normalizza gli spazi e stampa i titoli. Non è un sistema bibliotecario completo: il comportamento è deliberatamente piccolo, così possiamo seguire ogni passaggio dal repository all'artefatto. Nel capitolo 25 abbiamo già affrontato file, charset e risorse; qui la domanda è diversa. Vogliamo che una seconda persona possa partire dai file versionati, lanciare i controlli e ottenere un programma distribuibile senza indovinare istruzioni nascoste.
Definizione – Build riproducibile. In questo capitolo «riproducibile» significa che sorgenti, configurazione, versioni dichiarate, ambiente e comandi permettono di ricostruire e verificare il comportamento atteso. Non promettiamo automaticamente byte identici in ogni JAR: timestamp degli archivi, plugin, sistema operativo e altri input possono influire sui byte finali. Se l'identità binaria è un requisito, va progettata e confrontata separatamente.
Prima separiamo ciò che scriviamo da ciò che generiamo
La figura 29.1 mostra il percorso del nostro esempio. I sorgenti di produzione sono sotto src/main/java; i test sotto src/test/java; il file dimostrativo sta in dati/. Maven scrive classi, rapporti e JAR in target/. Quest'ultima cartella è un risultato, non una fonte da correggere a mano. Se modifichiamo un .class o un rapporto dentro target, la prossima build lo sostituirà e nessun collega ritroverà la modifica nel sorgente.
Nel progetto completo catalogo troviamo tre classi di produzione, due classi di test, il file dimostrativo e pom.xml. Un progetto reale può avere risorse in src/main/resources e dati di prova in src/test/resources; non creiamo quelle directory vuote solo per rispettare un disegno. La distinzione importante è la responsabilità: un test può usare JUnit senza trascinare JUnit nel programma distribuito, e un file generato non viene scambiato per il sorgente della verità.
catalogo/
pom.xml
dati/titoli.txt
src/main/java/esempio/catalogo/
Titoli.java ArchivioTitoli.java CatalogoCli.java
src/test/java/esempio/catalogo/
TitoliTest.java ArchivioTitoliTest.java
target/ ← creato dalla build
Concetto chiave – Tre livelli. Il JDK contiene
javac,java,jar,javadoc,jdeps,jlinke altri strumenti. Maven è un programma esterno che organizza fasi e dipendenze, chiamando anche strumenti del JDK tramite plugin. JUnit è una dipendenza esterna che offre il modello dei test e il motore per eseguirli. Una pipeline di integrazione continua ripete i comandi in un ambiente dichiarato. La figura 29.2 distingue questi livelli: un errore di compilazione, un test fallito e un artefatto mancante chiedono diagnosi diverse.
Prima ancora di Maven: compilare e capire i percorsi
Il compilatore javac legge i .java e produce file .class per la JVM. Se siamo nella cartella catalogo, possiamo isolare i file generati con questo comando:
mkdir -p target/manuale
javac --release 25 -Xlint:all -d target/manuale \
src/main/java/esempio/catalogo/*.java
java -cp target/manuale esempio.catalogo.CatalogoCli dati/titoli.txt
-d target/manuale indica dove mettere le classi. Il loro percorso interno segue il package esempio.catalogo: il compilatore crea esempio/catalogo sotto la directory di output. -cp o --class-path dice al launcher dove cercare le classi dell'applicazione. --release 25 seleziona il livello di linguaggio, il formato delle classi e le API della release dichiarata per la compilazione; non installa Java 25 su un altro computer e non rende disponibili da sola librerie di terze parti. -Xlint:all rende visibili categorie di avvisi utili, da leggere e correggere o motivare. Una build che sopprime tutti gli avvisi per ottenere una schermata pulita perde informazione.
Nel capitolo 6 abbiamo visto il module path. Il nostro esempio resta sul class path per rendere il percorso di build iniziale trasparente; il JAR ottenuto non è un modulo nominato. Se un progetto dichiara module-info.java, compilazione, risoluzione dei moduli e comando di avvio cambiano: occorre progettare requires ed exports, non aggiungere --module-path a caso. jlink, che useremo più avanti, può creare un runtime anche per questa applicazione sul class path scegliendo i moduli del JDK necessari; ciò non trasforma il JAR in un modular JAR.
L'esecuzione prevista stampa Titoli letti: 2, poi Java moderno ed È tempo di imparare. Il secondo titolo verifica che la lettura sia davvero UTF-8 nel caso di prova, ma un solo file positivo non prova che qualsiasi input sia accettabile. Nel capitolo 30 torneremo proprio sul confine dei dati non fidati.
Le tre classi del caso di studio
La prima classe, Titoli, rappresenta una regola di dominio piccola ma verificabile. Riceve una stringa, elimina gli spazi ai bordi, riduce sequenze interne di spazi e rifiuta un titolo vuoto o oltre ottanta unità char Java. Il limite è una regola didattica, non una norma universale per i titoli dei libri. String.length() conta unità UTF-16, non necessariamente caratteri percepiti da una persona: se il dominio richiede un altro criterio, il test e la regola devono cambiarlo esplicitamente.
package esempio.catalogo;
import java.util.Objects;
/** Regole didattiche per i titoli del piccolo catalogo. */
public final class Titoli {
private Titoli() {
}
/**
* Elimina spazi ai bordi e riduce sequenze di spazi interni.
*
* @param ingresso titolo da normalizzare
* @return titolo non vuoto e lungo al massimo 80 caratteri Java
* @throws NullPointerException se ingresso è null
* @throws IllegalArgumentException se il titolo è vuoto o troppo lungo
*/
public static String normalizza(String ingresso) {
Objects.requireNonNull(ingresso, "ingresso");
String titolo = ingresso.strip().replaceAll("\\s+", " ");
if (titolo.isEmpty() || titolo.length() > 80) {
throw new IllegalArgumentException("Titolo vuoto o troppo lungo");
}
return titolo;
}
}
Objects.requireNonNull rende distinto il caso null da una stringa vuota. strip() rimuove spazi ai bordi secondo le regole Unicode del metodo; replaceAll("\\s+", " ") segue invece la definizione di \s dell'espressione regolare Java usata qui. Non promettiamo con questa riga di normalizzare qualunque forma di spazio Unicode in modo identico: se il catalogo riceve dati internazionali complessi, specifichiamo i caratteri da accettare e aggiungiamo esempi. La Javadoc del metodo documenta il contratto pubblico e le eccezioni, così una modifica della regola può essere riconosciuta nei test e nella documentazione generata.
La seconda classe, ArchivioTitoli, collega quella regola al file. Files.lines restituisce uno stream che usa una risorsa da chiudere: il try-with-resources la chiude anche se una riga provoca un'eccezione. Le righe vuote vengono saltate, le altre passano per Titoli.normalizza. Il risultato è una copia non modificabile della lista raccolta; non significa che il file originale non possa cambiare dopo la lettura.
package esempio.catalogo;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Legge un archivio didattico con un titolo per riga. */
public final class ArchivioTitoli {
private ArchivioTitoli() {
}
/**
* Restituisce i titoli non vuoti nell'ordine del file.
*
* @param percorso file UTF-8 con un titolo per riga
* @return lista non modificabile dei titoli normalizzati
* @throws IOException se la lettura fallisce
*/
public static List<String> leggi(Path percorso) throws IOException {
List<String> titoli = new ArrayList<>();
try (var righe = Files.lines(percorso, StandardCharsets.UTF_8)) {
for (String riga : (Iterable<String>) righe::iterator) {
if (!riga.isBlank()) {
titoli.add(Titoli.normalizza(riga));
}
}
}
return List.copyOf(titoli);
}
}
Il ciclo usa un adattamento Iterable per percorrere lo stream nel blocco che lo chiude. È una scelta locale dell'esempio: in un progetto si potrebbe usare una pipeline Stream, purché la gestione della risorsa resti visibile. Questa versione accumula tutti i titoli in memoria e non impone un limite al numero di righe né alla dimensione della riga letta. Va bene per un file dimostrativo controllato; non è ancora un importatore sicuro per file arbitrari. Il capitolo 30 mostrerà come cambiare il requisito senza fingere che una build verde equivalga a una verifica di sicurezza.
La terza classe, CatalogoCli, è il punto d'ingresso. Riceve il percorso del file come argomento, chiama l'importazione e stampa il risultato. La scelta di richiedere esattamente un argomento rende ripetibile la prova. Un'applicazione per utenti finali dovrebbe tradurre un errore di input o di lettura in un messaggio e in un codice di uscita appropriato; qui lasciamo visibile la distinzione fra fallimento della build e fallimento a tempo di esecuzione.
package esempio.catalogo;
import java.io.IOException;
import java.nio.file.Path;
/** Punto di ingresso dell'esempio di build e distribuzione. */
public final class CatalogoCli {
private CatalogoCli() {
}
/**
* Legge e stampa i titoli del file indicato.
*
* @param args un percorso di file
* @throws IOException se la lettura fallisce
*/
public static void main(String[] args) throws IOException {
if (args.length != 1) {
throw new IllegalArgumentException("Uso: CatalogoCli <file-titoli>");
}
var titoli = ArchivioTitoli.leggi(Path.of(args[0]));
System.out.println("Titoli letti: " + titoli.size());
for (String titolo : titoli) {
System.out.println(titolo);
}
}
}
Perché introdurre Maven
Abbiamo appena compilato manualmente tre file. Potremmo fare lo stesso con i test, ma dovremmo trovare JUnit, includere ogni suo JAR e avviare il motore corretto. Poi dovremmo ripetere comandi coerenti per documentazione, pacchetto e analisi. Un sistema di build registra questa sequenza. Maven usa un file di progetto chiamato POM, pom.xml, che descrive coordinate, dipendenze, plugin e configurazione. Le coordinate groupId:artifactId:version identificano il nostro artefatto; nel caso di studio sono esempio:catalogo:1.0.0.
Nel POM completo fissiamo la release del compilatore a 25, JUnit 5.14.1 per i soli test, e le versioni dei plugin che ci servono. Dichiarare le versioni è una parte della ripetibilità: affidarsi alle versioni implicite del computer o del parent POM può cambiare il comportamento quando quell'ambiente cambia. Il file non nasconde che Maven e JUnit sono strumenti esterni a Java SE.
<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>esempio</groupId>
<artifactId>catalogo</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
<junit.version>5.14.1</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.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>esempio.catalogo.CatalogoCli</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
<scope>test</scope> impedisce che JUnit diventi una dipendenza ordinaria dell'applicazione in esecuzione. Il plugin compiler usa --release 25; Surefire scopre ed esegue i test Jupiter; il plugin JAR scrive nel manifest la classe principale. Se un test non compare nel rapporto, il problema non si risolve contando soltanto le righe BUILD SUCCESS: occorre controllare convenzioni dei nomi, motore di test e rapporto dei test eseguiti. Nel nostro caso la build riporta cinque invocazioni di test. In una macchina priva delle dipendenze nella cache Maven, la prima build deve poter raggiungere un repository autorizzato o un mirror aziendale; l'opzione -o usata nella verifica locale funziona soltanto perché gli artefatti sono già presenti.
mvn clean verify chiede prima di eliminare l'output precedente e poi percorre le fasi necessarie fino a verify. Maven esegue i goal associati alle fasi secondo il POM e il tipo di packaging. La parola verify non aggiunge per magia test di integrazione o controlli di sicurezza: li esegue solo se sono stati configurati nel ciclo. mvn test ferma il percorso ai test; mvn package costruisce il JAR; mvn install copia l'artefatto nel repository locale del computer. deploy lo invia a un repository remoto configurato: non è un sinonimo di «eseguire l'applicazione». Per una prova pulita del progetto, dalla sua cartella usiamo:
mvn clean verify
java -jar target/catalogo-1.0.0.jar dati/titoli.txt
Approfondimento – Maven e Gradle risolvono lo stesso bisogno organizzativo. Gradle descrive attività e dipendenze con un modello diverso, mentre Maven ha convenzioni e fasi dichiarate nel POM. In questo volume usiamo Maven come percorso riproducibile unico, così codice, test e output restano verificabili con una sola configurazione. Chi usa Gradle deve tradurre i requisiti, non copiare letteralmente le fasi Maven: release Java 25, separazione dei test, versioni fissate, JAR avviabile e prova su ambiente pulito rimangono gli stessi obiettivi. Il wrapper dello strumento e la verifica delle distribuzioni sono parti del progetto quando si adotta Gradle.
Una dipendenza arriva spesso con altre dipendenze
JUnit compare nel POM come dipendenza diretta. Le sue componenti possono portare altre librerie transitive: Maven legge i POM delle dipendenze e costruisce un grafo. Se due rami chiedono versioni diverse della stessa libreria, la mediazione della versione può scegliere un risultato che il lettore non si aspettava. Perciò «ho dichiarato solo una libreria» non significa «il progetto usa un solo artefatto esterno». mvn dependency:tree è il primo modo di osservare il grafo quando il plugin necessario è disponibile; si registrano anche repository, versioni risolte e politica di aggiornamento. Un scope sbagliato può lasciare fuori dal runtime una libreria che il programma usa, oppure portare nel pacchetto una libreria usata solo nei test.
La risoluzione non è una garanzia di fiducia. Un repository può essere indisponibile; un artefatto può essere sostituito o compromesso; una versione fissata può contenere una vulnerabilità nota. Per una build professionale, fissiamo origine e integrità secondo gli strumenti disponibili, conserviamo un inventario delle dipendenze e rivalutiamo gli aggiornamenti. Il capitolo 30 riprenderà la catena di fornitura come problema di sicurezza e manutenzione. Qui ci basta imparare a leggere il POM come parte del codice del progetto, non come un file amministrativo da lasciare a chi «si occupa della build».
Testare il comportamento, non soltanto eseguire metodi
JUnit non è incluso nel JDK. Lo usiamo perché descrive casi e risultati attesi in un formato eseguibile e leggibile. Il test di Titoli copre due input positivi con un test parametrizzato e due limiti negativi. @CsvSource fornisce coppie ingresso–atteso; @Test marca gli altri casi; assertThrows verifica che un titolo vuoto o lungo 81 unità venga rifiutato. La ripetizione dei casi positivi serve a mostrare che la stessa regola si trasferisce a stringhe diverse, non a gonfiare il numero di test.
package esempio.catalogo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class TitoliTest {
@ParameterizedTest
@CsvSource({"' Java moderno ', 'Java moderno'",
"'Il mattone', 'Il mattone'"})
void normalizzaSpazi(String ingresso, String atteso) {
assertEquals(atteso, Titoli.normalizza(ingresso));
}
@Test
void rifiutaTitoloVuoto() {
assertThrows(IllegalArgumentException.class,
() -> Titoli.normalizza(" "));
}
@Test
void rifiutaTitoloTroppoLungo() {
assertThrows(IllegalArgumentException.class,
() -> Titoli.normalizza("a".repeat(81)));
}
}
Il test di ArchivioTitoli usa @TempDir. JUnit crea una directory di prova, e il test scrive lì un file UTF-8: non dipende da un percorso personale, da un file lasciato da un'esecuzione precedente o dall'ordine dei test. Qui vengono esercitati insieme file system, decodifica, filtro delle righe e normalizzazione. È più vicino a un test d'integrazione fra componenti locali rispetto ai test puri di Titoli, pur restando piccolo e veloce. La parola «integrazione» non impone automaticamente un database o un server remoto.
package esempio.catalogo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class ArchivioTitoliTest {
@TempDir Path cartella;
@Test
void leggeRigheNonVuoteInUtf8() throws IOException {
Path file = cartella.resolve("titoli.txt");
Files.writeString(file, " Java moderno \n\nÈ tempo di imparare\n",
StandardCharsets.UTF_8);
assertEquals(List.of("Java moderno", "È tempo di imparare"),
ArchivioTitoli.leggi(file));
}
}
Il test unitario risponde a una domanda stretta: a questo input, la funzione produce il risultato o l'errore pattuito? Il test d'integrazione controlla che più parti collaborino. La prova di sistema avvia il JAR come lo farà un utente e verifica manifest, class path e file reale. Nel nostro caso java -jar ... dati/titoli.txt è proprio quella prova di fumo: se fallisce, i test unitari verdi non salvano l'artefatto. Una pipeline robusta esegue controlli nei livelli che corrispondono ai rischi, senza trasformare la «piramide dei test» in una legge sul numero esatto di prove per categoria.
Per test deterministici, il tempo va introdotto tramite Clock, come in C26, invece di leggere Instant.now() nel cuore della regola. I file temporanei devono essere creati e ripuliti dal test. Le prove di rete di C27 usano server locali controllati, non un sito pubblico che può cambiare o essere irraggiungibile. La concorrenza di C20 e C20A richiede verifiche di invarianti, non confronti con l'ordine di stampa di thread diversi. Questo non elimina i difetti intermittenti, ma evita di aggiungere instabilità artificiale ai test.
Concetto chiave – Copertura non è correttezza. Una percentuale di righe eseguite dice quali parti del codice sono state attraversate dal test. Non dice se l'asserzione riconoscerebbe un risultato sbagliato, se un limite è stato provato o se il requisito era corretto. Se un test chiama
normalizza("Java")senza confrontare il risultato, può aumentare la copertura senza controllare quasi nulla. Un modo utile di giudicare la prova è chiedere quale modifica difettosa farebbe fallire quel test. La mutation testing può automatizzare parte di questo esperimento, ma anche i mutanti uccisi non sostituiscono la scelta dei requisiti da verificare.
Dal codice compilato al JAR
Un JAR è un archivio che contiene classi e risorse con una struttura definita. jar --list --file target/catalogo-1.0.0.jar mostra le tre classi del programma, il manifest e metadati Maven. I test non sono nel JAR ordinario, perché si trovano nell'output di test. Nel manifest, Main-Class indica esempio.catalogo.CatalogoCli: è ciò che permette java -jar. Senza quella voce, un JAR può essere valido come libreria e tuttavia non avviarsi con quel comando. La figura 29.3 distingue tre risultati spesso confusi: JAR, runtime image e pacchetto applicativo.
Il JAR dell'esempio non contiene JUnit e non è un fat JAR che ingloba tutte le dipendenze esterne. Può avviarsi da solo perché il programma di produzione usa soltanto API del JDK. Se aggiungessimo una libreria di produzione, dovremmo scegliere come distribuirla: class path accanto al JAR, layout applicativo, modulo o pacchetto che la includa. Il manifest non scarica le dipendenze al momento dell'avvio. Guardare il contenuto dell'artefatto è quindi una verifica, non una curiosità.
Documentazione e strumenti di analisi del JDK
La Javadoc di Titoli.normalizza dichiara ingresso, valore restituito ed errori. javadoc può generare pagine HTML a partire dai commenti /** ... */; un commento che ripete il nome del metodo senza spiegare il contratto non aiuta. javadoc -d target/docs src/main/java/esempio/catalogo/*.java produce la documentazione in una cartella generata. La verifica della documentazione deve controllare avvisi e link, oltre a leggere la pagina prodotta: una pagina HTML esistente può comunque descrivere una regola obsoleta. javap -c -p -classpath target/classes esempio.catalogo.Titoli mostra il bytecode e i membri della classe compilata; non è un sostituto della lettura del sorgente, ma aiuta a capire che cosa è finito nell'artefatto.
jdeps --print-module-deps target/catalogo-1.0.0.jar riporta java.base per questo caso. È un'analisi delle dipendenze staticamente osservabili verso moduli della piattaforma. Non può dedurre sempre un plugin caricato per nome, una classe cercata da configurazione o una libreria nativa aperta più tardi. jdeprscan --release 25 target/catalogo-1.0.0.jar cerca usi di API deprecate nel bytecode secondo la release scelta; l'assenza di righe nel nostro caso non prova assenza di ogni problema di migrazione. Quando un'analisi produce un risultato, lo colleghiamo alla riga di codice o alla dipendenza che lo causa invece di usare il numero di avvisi come unico indicatore.
jshell è utile per provare una chiamata o un'ipotesi con feedback immediato. Un debugger permette di fermarsi su una riga e osservare valori e stack; jdb è l'interfaccia a riga di comando del JDK, mentre gli IDE offrono viste più comode. Nessuno dei due sostituisce un test da conservare: l'esperimento interattivo diventa conoscenza del progetto quando trasformiamo l'osservazione in un caso ripetibile. Nel capitolo 30 useremo jcmd e Java Flight Recorder per diagnosticare un processo già in esecuzione; anche lì la domanda precede il comando.
Un runtime per il programma
Una macchina che riceve il solo JAR ha comunque bisogno di un runtime Java compatibile. jlink costruisce una runtime image a partire dai moduli JDK scelti. Nel nostro caso jdeps indica java.base, e il programma non carica altri moduli dinamicamente. Dalla cartella catalogo possiamo provare:
jlink --add-modules java.base --output target/runtime
target/runtime/bin/java -jar target/catalogo-1.0.0.jar dati/titoli.txt
La seconda riga è la prova decisiva: non basta che jlink crei una directory. Un'applicazione che usa reflection, provider di servizi, charset particolari o funzionalità caricate dinamicamente può richiedere moduli che jdeps non vede nel percorso statico. Il runtime va provato con i casi d'uso reali su un ambiente pulito e sulla piattaforma di destinazione. Una runtime image creata per macOS non è automaticamente un runtime per Windows o Linux: i binari del JDK dipendono dalla piattaforma.
jpackage può combinare l'applicazione e un runtime in un'immagine applicativa o in un formato installabile supportato dal sistema operativo. Sul Mac usato per la verifica del libro il seguente comando ha prodotto un'app image e il relativo launcher ha eseguito il file di prova:
jpackage --type app-image --name CatalogoDidattico \
--input target --main-jar catalogo-1.0.0.jar \
--runtime-image target/runtime --dest target/pacchetto
La posizione del launcher e il formato del pacchetto cambiano con il sistema operativo. Per distribuire a terzi, oltre a lanciare il programma occorre decidere versione, firma, provenienza, licenze, aggiornamento e rimozione. Un'app image locale non equivale a un installer firmato e verificato su tutte le piattaforme. Il comando mostra il passo tecnico, non sostituisce una politica di rilascio.
Il controllo che un collega può ripetere
Integrazione continua significa far ripetere a una macchina, a ogni cambiamento proposto, i controlli definiti dal progetto. Per il nostro catalogo una pipeline minima fa checkout del repository, seleziona il JDK 25, esegue mvn -B clean verify, conserva rapporti dei test e JAR, poi svolge una prova di avvio dell'artefatto. -B chiede una modalità adatta a un ambiente non interattivo; non cambia la semantica dei test. Il job deve sapere da dove arrivano JDK, Maven e dipendenze, e quale ambiente di rete o cache usa. Se la build locale funziona solo grazie a un file installato a mano, la pipeline rivela un input mancante.
| Passo | Evidenza osservabile | Domanda se fallisce |
|---|---|---|
| Compilazione | classi e avvisi dichiarati | Quale sorgente o API non coincide con Java 25? |
| Test | numero di casi, errori, rapporti | La regola è sbagliata o il test dipende dall'ambiente? |
| Pacchetto | JAR e manifest ispezionati | La classe principale e le dipendenze sono presenti? |
| Avvio | output del JAR e dell'immagine runtime | Il programma parte senza l'ambiente di sviluppo? |
| Analisi | moduli, deprecazioni, dipendenze | Quale rischio è reale e dove va corretto? |
Il controllo non deve interrompersi alla scritta BUILD SUCCESS: se Surefire esegue zero test, il requisito «test eseguiti» non è soddisfatto. Analogamente, una build può riuscire pur producendo un JAR non avviabile o un programma che non trova un file nel pacchetto. Per questo il caso di studio ha più prove, ciascuna legata a una promessa precisa. Le attività del capitolo 29 chiedono di introdurre un difetto alla volta e identificare in quale passaggio dovrebbe emergere.
Trasferiamo il metodo
Immaginiamo di sostituire il file locale con un client HTTP del capitolo 27. Il codice di dominio resta in src/main/java, ma il test che usa una risposta di rete deve controllare un server locale e i suoi tempi. Il runtime potrebbe richiedere moduli diversi; il pacchetto dovrebbe conservare configurazione e certificati in modo appropriato. Oppure immaginiamo di trasformare il catalogo in un modulo nominato: il pom.xml, il JAR e il comando di avvio dovrebbero riflettere module-info.java. In entrambi i casi non copiamo alla cieca l'ultimo comando del capitolo: ricostruiamo il grafo di input, prove e output che l'altro programmatore deve poter seguire.
La domanda con cui chiudiamo è semplice: quali file e quali istruzioni servono a ricostruire il comportamento che abbiamo promesso? Se non possiamo rispondere guardando repository, POM, test e rapporto di build, manca ancora un mattone. C30 riparte da qui: una build ripetibile ci dà una base affidabile per cercare difetti, proteggere confini e osservare il programma mentre lavora.
Riferimenti essenziali
Riferimento essenziale – Strumenti e librerie. Le specifiche Oracle di
javac,jar,javadoc,jdeps,jdeprscan,jlinkejpackagedefiniscono i comandi usati nel JDK 25. Apache Maven documenta il ciclo di build e la risoluzione delle dipendenze. La guida JUnit 5.14.1 documenta i test parametrizzati del progetto. Queste fonti precisano i contratti degli strumenti; i comandi del caso di studio sono stati verificati nell'ambiente dichiarato e devono essere riprovati sulla piattaforma di distribuzione.