I test unitari sono un metodo di verifica del software in cui si testa la correttezza di singoli moduli o funzioni di codice in isolamento dal resto del sistema. Secondo Martin Fowler, 2026, i test unitari sono il fondamento di CI/CD e refactoring, fornendo un feedback rapido sul funzionamento del codice. Il test modulare aiuta a rilevare errori nelle fasi iniziali dello sviluppo, riducendo il costo della loro correzione di decine di volte.
Punti chiave
I test unitari sono il processo di verifica di unità individuali del codice sorgente — funzioni, metodi, classi — in isolamento dal resto del programma. Ogni test esegue uno scenario d'uso specifico del modulo e verifica che il risultato corrisponda a quello atteso. I test unitari sono scritti nello stesso linguaggio di programmazione del codice principale e vengono eseguiti automaticamente nell'ambiente di sviluppo o in una pipeline CI/CD. A differenza dei test di integrazione, i test unitari non interagiscono con database reali, file system o servizi di rete.
L'obiettivo principale è il feedback rapido sulla correttezza del codice dopo le modifiche. Se uno sviluppatore refactoring un metodo, la suite di test unitari conferma che il comportamento non è stato alterato. Secondo Google Testing Blog (2025), i progetti con copertura dei test unitari superiore al 60% hanno 2,5 volte meno incidenti in produzione. Vantaggi aggiuntivi: documentazione del codice (i test mostrano come usare l'API), semplificazione del refactoring (l'implementazione può essere modificata mantenendo il comportamento) e diagnosi rapida delle regressioni.
Non tutti i test automatizzati sono test unitari. Criteri: viene testato un singolo modulo (classe o funzione), le dipendenze esterne vengono sostituite con mock o stub, il test viene eseguito in millisecondi e non richiede l'avvio di un server o database. Un test che accede a un database reale è un test di integrazione. Un test che apre un browser è un test E2E. Comprendere i confini tra i tipi di test è importante per distribuire correttamente gli sforzi nella piramide dei test.
I test unitari di qualità seguono i principi FIRST formulati da Robert C. Martin. Ogni test dovrebbe essere Fast (veloce — millisecondi), Isolated (isolato — non dipende da altri test), Repeatable (ripetibile — stesso risultato su qualsiasi macchina), Self-validating (autoverificante — risultato "superato" o "fallito", senza controllo manuale) e Timely (tempestivo — scritto prima o contemporaneamente al codice). Violare qualsiasi principio riduce il valore del test.
Un modello standard per scrivere test unitari. Arrange — preparazione dei dati e delle dipendenze: creazione di oggetti, configurazione dei mock, impostazione dei parametri di input. Act — esecuzione dell'azione testata: chiamata di un metodo o funzione. Assert — verifica del risultato: confronto del valore effettivo con quello atteso. La divisione in tre blocchi rende il test leggibile e comprensibile. Se il blocco Assert richiede logica complessa, il test sta probabilmente verificando troppe cose contemporaneamente.
// Esempio di test unitario con pattern AAA in Kotlin con JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — creare l'oggetto di test
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — eseguire l'azione
val result = calculator.add(2, 3)
// ASSERT — verificare il risultato
Assertions.assertEquals(5, result)
}
}
Il nome del test dovrebbe descrivere cosa viene verificato e quale risultato è atteso. Formato: [methodName]_[scenario]_[expectedResult]. Esempio: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Un buon nome di test sostituisce un commento e, in caso di fallimento, indica immediatamente quale funzionalità è stata compromessa. Evitate nomi come test1, checkSomething o verify — non portano informazioni e complicano la diagnostica.
Per isolare il modulo testato dalle dipendenze esterne, vengono utilizzati i doppi di test (test doubles). Tipi principali: mock — verificano che un metodo specifico sia stato chiamato con i parametri attesi; stub — restituiscono valori predefiniti quando viene chiamato un metodo; fake — implementazioni semplificate di componenti reali (ad esempio, InMemoryUserRepository invece di UserRepository che lavora con un database). La scelta dipende da ciò che deve essere verificato: stato (stub) o interazione (mock).
| Doppio | Cosa verifica | Esempio |
|---|---|---|
| Mock | Chiamata del metodo con parametri corretti | userRepository.save(user) è stato chiamato esattamente 1 volta |
| Stub | Valore di ritorno | repository.findById(1) restituisce User(id=1, name="Test") |
| Fake | Logica tramite implementazione semplificata | InMemoryMapUserRepository con HashMap invece di DB |
| Spia | Mocking parziale di un oggetto reale | spy(repo).when(findById).thenReturn(user) |
Mockito è il framework di mocking più popolare per Java e Kotlin. Permette di creare mock tramite mock(), configurare valori di ritorno tramite when().thenReturn() e verificare chiamate tramite verify(). Le versioni moderne di Mockito (5.x) supportano mock statici (mockStatic) e sintassi semplificata tramite BDDMockito (given-willReturn). Una regola importante: non mockare ciò che non è tuo — non creare mock per oggetti di valore e librerie standard.
// Esempio di test unitario con Mockito in Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) è una metodologia in cui il test viene scritto prima dell'implementazione del codice. Il ciclo "Rosso-Verde-Refactor": scrivere un test che fallisce (Rosso), scrivere il codice minimo per superare il test (Verde), migliorare il codice senza cambiare il comportamento (Refactor). TDD garantisce che tutto il codice sia coperto da test (copertura = 100% per le funzionalità implementate) e che il codice sia testabile — se il codice è difficile da testare, l'architettura necessita di miglioramenti.
Secondo una ricerca di IBM (2006-2026, studio longitudinale), i team che usano TDD hanno dal 40 all'80% in meno di difetti in produzione rispetto ai team che scrivono test dopo il codice. TDD migliora anche l'architettura: lo sviluppatore è costretto a pensare al design dell'API prima dell'implementazione, portando a basso accoppiamento (loose coupling) e alta coesione (high cohesion). Un effetto aggiuntivo è la documentazione vivente: i test fungono da specifica del comportamento del modulo, sempre aggiornata.
TDD non è sempre ottimale. I componenti dell'interfaccia utente sono difficili da testare isolatamente — per essi sono più efficaci i test snapshot o i test di regressione visiva (Percy, Chromatic). Il prototipazione e la ricerca (spike solutions) non richiedono test. Il codice legacy senza test è difficile da coprire con TDD — qui sono prima necessari test di caratterizzazione (test che catturano il comportamento attuale prima del refactoring). In questi casi, TDD non viene completamente abbandonato ma adattato — i test vengono scritti per la funzionalità modificata, non per l'intero codice legacy.
Lo sviluppo mobile ha le sue specificità: la logica di business è spesso mescolata con il codice dell'interfaccia utente (Activity, ViewController, ViewModel), complicando i test unitari. La pratica migliore è View sottili, ViewModel spessi: estrarre tutta la logica dai componenti dell'interfaccia utente in classi separate (UseCase, Repository, ViewModel) facili da testare senza emulatore. Android e iOS hanno framework nativi di test unitari che funzionano su JVM/Native senza avviare un dispositivo.
I test unitari Android vengono eseguiti su una JVM locale senza emulatore, offrendo velocità di esecuzione — un test tipico richiede meno di 100 ms. JUnit 5 è il runner principale. Per i test ViewModel, utilizzate kotlinx-coroutines-test per testare le coroutine e Turbine per testare StateFlow. Robolectric permette di testare componenti dipendenti da Android (Context, Resources) senza emulatore caricando classi shadow. Per i test Compose, utilizzate Compose UI Test — ma questi sono test dell'interfaccia utente, non test unitari.
I test unitari iOS sono scritti in Swift con XCTest (integrato in Xcode). Quick + Nimble sono framework BDD per test più leggibili (describe/context/it). Per il mocking, utilizzate Cuckoo (generazione di mock) o SwiftyMocky. Swift supporta i protocolli e l'iniezione di dipendenze, facilitando la sostituzione delle dipendenze. Punto chiave: i test unitari iOS vengono eseguiti sul simulatore macOS, non su un dispositivo reale. I test che richiedono funzionalità hardware (fotocamera, Bluetooth) sono test di integrazione.
I test unitari Flutter usano il pacchetto flutter_test e vengono eseguiti sulla Dart VM senza emulatore. Per il mocking, utilizzate il pacchetto mockito con generazione di codice (build_runner). I test dei widget (nello stesso pacchetto) testano widget individuali ma richiedono rendering e sono più lenti — usateli solo per la verifica della logica dell'interfaccia utente. La logica Dart pura (modelli, repository, blocs) viene testata come normali test Dart senza importare flutter_test.
// Esempio di test unitario in Flutter con mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
I test unitari efficaci richiedono disciplina. La regola principale: testate il comportamento, non l'implementazione. Il test non dovrebbe sapere come il modulo è implementato internamente (quali metodi privati vengono chiamati, in quale ordine). Se un test è legato all'implementazione, si rompe a ogni refactoring e perde valore. Il test verifica il contratto: con input X, l'output dovrebbe essere Y. L'eccezione sono i test per algoritmi con prestazioni critiche, dove l'ordine delle chiamate è importante.
La copertura del 100% è un obiettivo irraggiungibile e non necessario. Secondo Google Testing Blog (2025), il livello di copertura ottimale per i test unitari è del 70-80% delle linee di codice. La copertura del 100% è spesso raggiunta testando getter, setter e costruttori, il che non aggiunge valore. Concentratevi sulla logica di business critica: calcoli complessi, validazione, gestione degli errori, casi limite. Usate JaCoCo (Java), Coverage.py (Python), Istanbul (JS) per la misurazione e impostate una soglia in CI — fallimento del build con copertura inferiore al 60%.
I test unitari sono il primo stadio di qualsiasi pipeline CI/CD. Vengono eseguiti a ogni push nel repository, prima del build e del deploy. Il tempo medio di esecuzione della suite di test unitari non dovrebbe superare i 5 minuti — se superiore, i test smettono di essere "veloci" e gli sviluppatori smettono di eseguirli localmente. Separate i test in veloci (unitari) e lenti (integrazione) ed eseguiteli in diverse fasi della pipeline. Usate l'esecuzione parallela e fail-fast per accelerare.
Domande frequenti
Un test unitario verifica un singolo modulo isolatamente, sostituendo le dipendenze esterne con mock. Un test di integrazione verifica l'interazione tra più componenti reali (DB, API, file system). I test unitari vengono eseguiti in millisecondi, i test di integrazione in secondi. Nella piramide dei test, i test unitari occupano il 70%.
La scelta dipende dalla piattaforma: JUnit 5 per Java/Kotlin, XCTest per iOS/Swift, pytest per Python, Jest/Vitest per JavaScript/TypeScript, flutter_test per Flutter. Per il mocking, usate Mockito (Java), Cuckoo (iOS), unittest.mock (Python) o vitest.mock (JS). Tutti i framework moderni supportano test parametrizzati, asserzioni integrate ed esecuzione parallela.
Fast — il test viene eseguito in millisecondi. Isolated — non dipende da altri test o sistemi esterni. Repeatable — dà lo stesso risultato su qualsiasi macchina. Self-validating — verifica automaticamente il risultato. Timely — scritto prima o contemporaneamente al codice. Violare anche un solo principio riduce l'efficacia del test.
Sì, assolutamente. Il ViewModel contiene la logica di business — gestione degli eventi, trasformazione dei dati, gestione dello stato. Su Android, usate kotlinx-coroutines-test per le coroutine e Turbine per testare StateFlow. Su iOS, testate i Combine Publishers o async/await nel ViewModel. I test ViewModel sono puri test unitari eseguiti su JVM/macOS senza emulatore.
Le richieste di rete nei test unitari non vengono eseguite — vengono sostituite con mock del client HTTP. Su Android, usate MockWebServer (OkHttp) — avvia un server HTTP locale, preferibile ai mock perché riproduce l'interazione di rete reale. MockWebServer fornisce isolamento senza perdere realismo. Per iOS — OHHTTPStubs o URLProtocol per intercettare e sostituire le risposte.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche