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.