Introduzione al Testing in Java
Perché il testing è importante in Java, la piramide dei test e una panoramica dei principali framework di testing Java.
Il testing automatizzato è il modo in cui si dimostra che il codice fa quello che si pensa faccia—e si continua a dimostrarlo man mano che il codice cambia. Invece di eseguire il programma a mano e controllare l'output a occhio, si scrivono piccoli programmi che esercitano il proprio codice e verificano i risultati automaticamente. In Java questo ecosistema è costruito attorno a framework come JUnit e Mockito, ma le idee sottostanti—arrange, act, assert—sono abbastanza semplici da scrivere a mano. Questo capitolo traccia il panorama prima che i capitoli successivi approfondiscano ogni strumento.
Perché il testing automatizzato è importante
Un test è un controllo piccolo e ripetibile che verifica che un pezzo di codice si comporti correttamente. Il vantaggio non si ottiene alla prima esecuzione—lo si ottiene a ogni esecuzione successiva. Una volta che un comportamento è catturato in un test, qualsiasi modifica che lo rompe fallisce in modo evidente e immediato, invece di emergere come un bug in produzione settimane dopo. I test documentano anche l'intento: un test ben nominato dice cosa il codice dovrebbe fare.
// A test names a behavior, runs the code, and asserts the outcome.
@Test
void addsTwoPositiveNumbers() {
int result = Calculator.add(2, 3);
assertEquals(5, result); // fails the build if result != 5
}L'obiettivo è il feedback rapido. Una suite di test verde significa che si può fare refactoring con fiducia; una rossa indica esattamente cosa si è rotto.
La piramide dei test
I test si suddividono in livelli, solitamente rappresentati come una piramide. I test unitari si trovano alla base: molti, veloci, ognuno controlla una classe o un metodo in isolamento. I test di integrazione si trovano nel mezzo: meno numerosi, più lenti, verificano che i componenti funzionino insieme (il proprio codice insieme a un database, per esempio). I test end-to-end (E2E) si trovano in cima: pochi, i più lenti, guidano l'intera applicazione come farebbe un utente.
| Livello | Ambito | Velocità | Quantità | Strumenti Java |
|---|---|---|---|---|
| Unit | una classe/metodo | veloce (ms) | molti | JUnit, AssertJ |
| Integration | diversi componenti | media | alcuni | JUnit, Testcontainers |
| End-to-end | intero sistema | lento | pochi | Selenium, REST-assured |
La forma è importante: affidarsi ai test unitari economici e veloci per la maggior parte della copertura, e riservare i test E2E lenti e fragili a un numero limitato di percorsi utente critici.
Il pattern arrange–act–assert
Quasi ogni test, in qualsiasi framework, segue la stessa struttura in tre passi. Arrange: si preparano gli input e le eventuali dipendenze. Act: si invoca il codice in esame. Assert: si verifica che il risultato corrisponda a quanto atteso. Mantenere questi passi visivamente separati rende un test facile da leggere e da diagnosticare quando fallisce.
@Test
void rejectsBlankUsername() {
// Arrange
UserService service = new UserService();
// Act
boolean valid = service.isValidUsername(" ");
// Assert
assertFalse(valid);
}Un'asserzione che fallisce lancia un'eccezione, il framework la registra e l'esecuzione prosegue al test successivo—così un comportamento difettoso non nasconde mai gli altri.
JUnit, il runner standard
JUnit è il framework di unit-testing de facto per Java. Si annotano i metodi con @Test, JUnit li scopre tramite reflection, esegue ognuno e riporta pass/fail. Le asserzioni come assertEquals, assertTrue e assertThrows sono helper statici che fanno fallire il test quando l'aspettativa non è soddisfatta. I progetti reali eseguono JUnit tramite un build tool (il plugin Surefire di Maven o il task test di Gradle), non a mano.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void dividesNumbers() {
assertEquals(4, Calculator.divide(8, 2));
}
@Test
void throwsOnDivideByZero() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(1, 0));
}
}Poiché in questo runner non è disponibile alcun JAR di JUnit né un build tool, l'esempio seguente ricostruisce la stessa idea da zero—un piccolo harness che esegue controlli nominati e conta i pass e i fail, esattamente quello che @Test più assertEquals fanno internamente.
Cosa ricavare dall'esecuzione:
- Ogni chiamata a
assertEqualsè un caso di test—si preparano gli input, si agisce chiamandoaddoisBlank, e si verifica il risultato—rispecchiando esattamente quello che fa un metodo JUnit@Test. - Un controllo superato stampa
PASSe uno fallito stampaFAILcon sia il valore atteso che quello effettivo, che è la diagnostica fornita dai messaggi di asserzione di JUnit. - Il caso volutamente sbagliato (
expected 10 but got 5) mostra come appare un test rosso: l'harness continua a eseguire i controlli rimanenti invece di fermarsi al primo fallimento. - Il riepilogo conta 5 totali, 4 passati, 1 fallito—lo stesso report pass/fail che un test runner stampa alla fine di un'esecuzione.
- Poiché un test è fallito, il programma termina con
BUILD FAILURE, dimostrando perché un singolo test difettoso dovrebbe far fallire l'intera build in CI.
Come si incastrano i pezzi
Gli strumenti di testing Java si sovrappongono l'uno all'altro, dalle asserzioni grezze fino alla piena integrazione con il build:
- Le asserzioni (
assertEquals,assertThrows) stabiliscono cosa deve essere vero. - JUnit scopre ed esegue i metodi
@Teste riporta i risultati. - Mockito fornisce collaboratori fittizi in modo che un'unità possa essere testata in isolamento.
- Maven o Gradle integra la suite nel build, facendola fallire in presenza di qualsiasi test rosso.
- CI esegue il build a ogni push, in modo che il codice difettoso non raggiunga mai il branch principale.
Ogni capitolo successivo affronta un gradino di questa scala—prima le annotazioni e le asserzioni di JUnit, poi il mocking con Mockito, poi l'integrazione dei test in Maven e Gradle. Capire dove si colloca ogni strumento mantiene coerente l'intera storia del testing.