Introduzione al testing
Dopo aver introdotto nella prima parte i concetti generali di qualità del software, in questo articolo ci concentriamo sul testing, attività fondamentale per garantire la qualità del prodotto.
Livelli di testing
Unit Testing
Test di singole unità di codice (classi, metodi):
public class CalcolatoreTest {
@Test
public void testSomma() {
Calcolatore calc = new Calcolatore();
assertEquals(5, calc.somma(2, 3));
}
@Test
public void testDivisionePerZero() {
Calcolatore calc = new Calcolatore();
assertThrows(ArithmeticException.class, () -> {
calc.dividi(10, 0);
});
}
}
Integration Testing
Test dell'integrazione tra componenti:
@Test
public void testOrdineCompleto() {
// Test integrazione tra carrello, inventario e pagamento
Carrello carrello = new Carrello();
carrello.aggiungi(prodotto);
Ordine ordine = servizioOrdini.creaOrdine(carrello);
servizioInventario.aggiornaDisponibilita(ordine);
servizioPagamento.processaPagamento(ordine);
assertTrue(ordine.isCompletato());
}
System Testing
Test del sistema completo:
- Test funzionali end-to-end
- Test non funzionali (performance, sicurezza)
- Test di usabilità
Acceptance Testing
Test di accettazione da parte del cliente:
- Validazione requisiti
- Test scenari reali
- User Acceptance Testing (UAT)
Tecniche di testing
Black Box Testing
Test basato sulle specifiche senza conoscere l'implementazione:
- Test di equivalenza
- Analisi valori limite
- Test di stato
- Test basati su casi d'uso
White Box Testing
Test basato sulla struttura interna del codice:
- Statement coverage
- Branch coverage
- Path coverage
- Condition coverage
Grey Box Testing
Combinazione di black box e white box.
JUnit: il framework standard
JUnit è lo standard de facto per unit testing in Java.
Annotations principali
public class EsempioTest {
@BeforeAll
static void setupAll() {
// Eseguito una volta prima di tutti i test
}
@BeforeEach
void setup() {
// Eseguito prima di ogni test
}
@Test
void testNormale() {
// Un test normale
}
@Test
@Disabled("In sviluppo")
void testDisabilitato() {
// Test temporaneamente disabilitato
}
@AfterEach
void tearDown() {
// Eseguito dopo ogni test
}
@AfterAll
static void tearDownAll() {
// Eseguito una volta dopo tutti i test
}
}
Assertions
// Uguaglianza
assertEquals(expected, actual);
assertNotEquals(expected, actual);
// Booleani
assertTrue(condition);
assertFalse(condition);
// Null
assertNull(object);
assertNotNull(object);
// Eccezioni
assertThrows(Exception.class, () -> {
// codice che dovrebbe lanciare eccezione
});
// Array e collections
assertArrayEquals(expectedArray, actualArray);
assertIterableEquals(expectedList, actualList);
// Timeout
assertTimeout(Duration.ofSeconds(2), () -> {
// codice da eseguire entro il timeout
});
Test-Driven Development (TDD)
Approccio dove i test vengono scritti prima del codice:
1. Red: Scrivi un test che fallisce 2. Green: Scrivi il codice minimo per far passare il test 3. Refactor: Migliora il codice mantenendo i test verdi
// 1. Red - Il test fallisce
@Test
void testCalcolaScontoProdotto() {
Prodotto p = new Prodotto("Libro", 100);
assertEquals(90, p.calcolaSconto(10));
}
// 2. Green - Implementazione minima
public class Prodotto {
public double calcolaSconto(double percentuale) {
return prezzo * (1 - percentuale/100);
}
}
// 3. Refactor - Migliora mantenendo test verdi
Mock Objects
Per testare componenti in isolamento:
@Test
void testServizioConMock() {
// Crea mock del repository
UserRepository mockRepo = mock(UserRepository.class);
when(mockRepo.findById(1L)).thenReturn(Optional.of(testUser));
// Usa il mock nel test
UserService service = new UserService(mockRepo);
User user = service.getUser(1L);
assertEquals("Mario", user.getNome());
verify(mockRepo).findById(1L);
}
Code Coverage
Misura della percentuale di codice coperta dai test:
mvn clean test jacoco:report
Obiettivi di coverage
- Minimo accettabile: 70-80%
- Obiettivo: 85-90%
- Eccellenza: > 90%
Ma attenzione: alta coverage non garantisce test di qualità!
Test di performance
Per verificare prestazioni e scalabilità:
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void testPerformance() {
// Il test deve completare in < 100ms
servizio.operazioneVeloce();
}
JMH per micro-benchmark
@Benchmark
public void testPerformanceMetodo() {
// Codice da benchmarkare
}
Continuous Testing
Integrazione del testing nel processo di CI/CD:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: mvn test
- name: Upload coverage
run: bash <(curl -s https://codecov.io/bash)
Best Practices
1. Test indipendenti: Ogni test deve essere indipendente dagli altri 2. Test ripetibili: Stesso risultato ad ogni esecuzione 3. Test veloci: Unit test devono essere rapidi 4. Test leggibili: Nomi descrittivi, arrange-act-assert 5. Test manutenibili: Evitare duplicazione, usare helper 6. Test completi: Testare casi normali, edge case ed errori
Conclusioni
Il testing è un investimento fondamentale per la qualità del software. L'uso di JUnit, TDD, mock objects e continuous testing permette di costruire applicazioni robuste e manutenibili. Un buon suite di test è la migliore documentazione del comportamento atteso del sistema.
