Salta al contenuto principale

13 - Testing in Java: JUnit e Mockito

Testing in Java con JUnit e Mockito I test automatizzati sono fondamentali per garantire qualità e manutenibilità del codice.

Cosa ne pensi di questo articolo?

In Breve

Testing in Java con JUnit e Mockito I test automatizzati sono fondamentali per garantire qualità e manutenibilità del codice.

Testing in Java con JUnit e Mockito

I test automatizzati sono fondamentali per garantire qualità e manutenibilità del codice. JUnit è il framework standard per unit testing in Java, mentre Mockito permette di creare mock objects.

Cosa Imparerai

  • JUnit 5: annotazioni e asserzioni
  • Test parametrizzati
  • Mockito: mock, stub e verify
  • Test-Driven Development (TDD)
  • Best practices per testing

JUnit 5 - Fondamenti

Test Base con JUnit 5
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalcolatriceTest {

    private Calcolatrice calc;

    @BeforeEach
    void setUp() {
        calc = new Calcolatrice();
    }

    @Test
    @DisplayName("Addizione di due numeri positivi")
    void testAddizione() {
        assertEquals(5, calc.somma(2, 3));
    }

    @Test
    void testSottrazione() {
        assertEquals(2, calc.sottrai(5, 3));
    }

    @Test
    void testDivisionePerZero() {
        assertThrows(ArithmeticException.class, () -> {
            calc.dividi(10, 0);
        });
    }

    @Test
    @Disabled("Test disabilitato temporaneamente")
    void testDaImplementare() {
        fail("Non ancora implementato");
    }
}

class Calcolatrice {
    int somma(int a, int b) { return a + b; }
    int sottrai(int a, int b) { return a - b; }
    int dividi(int a, int b) { return a / b; }
}

Annotazioni JUnit 5

Annotazione Descrizione
@Test Marca un metodo come test
@BeforeEach Eseguito prima di ogni test
@AfterEach Eseguito dopo ogni test
@BeforeAll Eseguito una volta prima di tutti i test
@DisplayName Nome leggibile per il test
@Disabled Disabilita temporaneamente il test

Asserzioni Avanzate

Asserzioni JUnit
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.*;

class AsserzioniTest {

    @Test
    void asserzioniBase() {
        // Uguaglianza
        assertEquals(4, 2 + 2);
        assertNotEquals(5, 2 + 2);

        // Booleani
        assertTrue(3 > 2);
        assertFalse(2 > 3);

        // Null
        Object obj = null;
        assertNull(obj);
        assertNotNull(new Object());

        // Same (stessa istanza)
        String s1 = "hello";
        String s2 = s1;
        assertSame(s1, s2);
    }

    @Test
    void asserzioniCollezioni() {
        List<String> lista = Arrays.asList("A", "B", "C");

        assertEquals(3, lista.size());
        assertTrue(lista.contains("B"));

        // Array
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};
        assertArrayEquals(expected, actual);
    }

    @Test
    void asserzioniEccezioni() {
        Exception ex = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("Messaggio errore");
        });
        assertEquals("Messaggio errore", ex.getMessage());
    }

    @Test
    void asserzioniTimeout() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            Thread.sleep(500);  // Deve completare entro 2 secondi
        });
    }

    @Test
    void asserzioniRaggruppate() {
        Studente s = new Studente("Mario", 25);

        assertAll("Verifica studente",
            () -> assertEquals("Mario", s.getNome()),
            () -> assertEquals(25, s.getEta()),
            () -> assertTrue(s.getEta() > 0)
        );
    }
}

class Studente {
    private String nome;
    private int eta;
    Studente(String nome, int eta) { this.nome = nome; this.eta = eta; }
    String getNome() { return nome; }
    int getEta() { return eta; }
}

Test Parametrizzati

Test con Parametri
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class TestParametrizzatiDemo {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void testNumeriPositivi(int numero) {
        assertTrue(numero > 0);
    }

    @ParameterizedTest
    @ValueSource(strings = {"hello", "world", "java"})
    void testStringheNonVuote(String str) {
        assertFalse(str.isEmpty());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, 20, 30"
    })
    void testSomma(int a, int b, int risultato) {
        assertEquals(risultato, a + b);
    }

    @ParameterizedTest
    @MethodSource("fornisciNumeri")
    void testConMethodSource(int numero, boolean atteso) {
        assertEquals(atteso, numero % 2 == 0);
    }

    static Stream<Arguments> fornisciNumeri() {
        return Stream.of(
            Arguments.of(2, true),
            Arguments.of(3, false),
            Arguments.of(4, true)
        );
    }

    @ParameterizedTest
    @EnumSource(Mese.class)
    void testMesi(Mese mese) {
        assertNotNull(mese);
    }

    enum Mese { GENNAIO, FEBBRAIO, MARZO }
}

Mockito - Mock Objects

Mockito Fondamenti
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

// Interfaccia da mockare
interface UserRepository {
    User findById(Long id);
    void save(User user);
    List<User> findAll();
}

class User {
    private Long id;
    private String nome;

    User(Long id, String nome) {
        this.id = id;
        this.nome = nome;
    }

    Long getId() { return id; }
    String getNome() { return nome; }
}

class UserService {
    private final UserRepository repository;

    UserService(UserRepository repository) {
        this.repository = repository;
    }

    User getUser(Long id) {
        return repository.findById(id);
    }

    void createUser(User user) {
        repository.save(user);
    }
}

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository mockRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void testGetUser() {
        // Arrange: configura il mock
        User userAtteso = new User(1L, "Mario");
        when(mockRepository.findById(1L)).thenReturn(userAtteso);

        // Act: esegui il metodo
        User risultato = userService.getUser(1L);

        // Assert: verifica
        assertEquals("Mario", risultato.getNome());
        verify(mockRepository).findById(1L);
    }

    @Test
    void testCreateUser() {
        User nuovoUser = new User(2L, "Luigi");

        userService.createUser(nuovoUser);

        // Verifica che save sia stato chiamato
        verify(mockRepository, times(1)).save(nuovoUser);
    }

    @Test
    void testUserNonTrovato() {
        when(mockRepository.findById(99L)).thenReturn(null);

        User risultato = userService.getUser(99L);

        assertNull(risultato);
    }
}

Mockito Avanzato

Stub, Verify e ArgumentCaptor
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

class MockitoAvanzatoTest {

    @Test
    void testStubAvanzato() {
        List<String> mockList = mock(List.class);

        // Stub con any matcher
        when(mockList.get(anyInt())).thenReturn("elemento");
        assertEquals("elemento", mockList.get(5));

        // Stub con comportamento diverso
        when(mockList.size())
            .thenReturn(1)
            .thenReturn(2)
            .thenReturn(3);

        assertEquals(1, mockList.size());  // Prima chiamata
        assertEquals(2, mockList.size());  // Seconda
        assertEquals(3, mockList.size());  // Terza e successive

        // Stub che lancia eccezione
        when(mockList.get(100)).thenThrow(new IndexOutOfBoundsException());
    }

    @Test
    void testVerifyAvanzato() {
        List<String> mockList = mock(List.class);

        mockList.add("uno");
        mockList.add("due");
        mockList.add("uno");

        // Verifica numero chiamate
        verify(mockList, times(2)).add("uno");
        verify(mockList, times(1)).add("due");
        verify(mockList, never()).add("tre");
        verify(mockList, atLeast(1)).add(anyString());
        verify(mockList, atMost(3)).add(anyString());
    }

    @Test
    void testArgumentCaptor() {
        UserRepository mockRepo = mock(UserRepository.class);
        UserService service = new UserService(mockRepo);

        // Cattura l'argomento passato a save
        ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);

        service.createUser(new User(1L, "Test"));

        verify(mockRepo).save(captor.capture());
        User userCatturato = captor.getValue();

        assertEquals("Test", userCatturato.getNome());
    }

    @Test
    void testSpyParziale() {
        // Spy: oggetto reale con alcune parti mockate
        List<String> realList = new ArrayList<>();
        List<String> spyList = spy(realList);

        spyList.add("elemento");  // Chiama metodo reale
        assertEquals(1, spyList.size());

        // Override di un metodo specifico
        doReturn(100).when(spyList).size();
        assertEquals(100, spyList.size());
    }
}

Test-Driven Development (TDD)

Ciclo TDD
// TDD segue il ciclo: RED -> GREEN -> REFACTOR

// STEP 1 - RED: Scrivi il test PRIMA del codice
class CarrelloTest {

    @Test
    void carrelloVuotoHaTotaleZero() {
        Carrello carrello = new Carrello();
        assertEquals(0.0, carrello.getTotale(), 0.001);
    }

    @Test
    void aggiungiProdottoCalcolaTotale() {
        Carrello carrello = new Carrello();
        Prodotto p = new Prodotto("Libro", 29.99);

        carrello.aggiungi(p);

        assertEquals(29.99, carrello.getTotale(), 0.001);
    }

    @Test
    void applicaSconto10Percento() {
        Carrello carrello = new Carrello();
        carrello.aggiungi(new Prodotto("A", 100.0));

        carrello.applicaSconto(10);

        assertEquals(90.0, carrello.getTotale(), 0.001);
    }
}

// STEP 2 - GREEN: Scrivi il codice minimo per far passare il test
class Prodotto {
    String nome;
    double prezzo;

    Prodotto(String nome, double prezzo) {
        this.nome = nome;
        this.prezzo = prezzo;
    }
}

class Carrello {
    private List<Prodotto> prodotti = new ArrayList<>();
    private double scontoPercentuale = 0;

    void aggiungi(Prodotto p) {
        prodotti.add(p);
    }

    double getTotale() {
        double totale = prodotti.stream()
            .mapToDouble(p -> p.prezzo)
            .sum();
        return totale * (1 - scontoPercentuale / 100);
    }

    void applicaSconto(double percentuale) {
        this.scontoPercentuale = percentuale;
    }
}

// STEP 3 - REFACTOR: Migliora il codice mantenendo i test verdi

Best Practices

Regole per Test Efficaci

  • Un'asserzione logica per test: test focalizzati
  • Nomi descrittivi: descrivono cosa testano
  • AAA pattern: Arrange, Act, Assert
  • Test indipendenti: non dipendono dall'ordine
  • Test veloci: devono essere eseguiti spesso
  • Mocka solo le dipendenze esterne: database, API, file
  • Evita test fragili: non testare implementazione
  • Coverage non è tutto: qualità > quantità

Domande frequenti

Qual è il framework standard per unit testing in Java?

JUnit.

Cosa consente di creare mock objects con Mockito?

La possibilità di creare oggetti mock.

Qual è l'importanza dei test automatizzati nella programmazione?

Garantire qualità e manutenibilità del codice.

Che cosa significa Test-Driven Development (TDD)?

Un approccio di sviluppo software che prevede la scrittura dei test prima della codifica.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti