Test unitari nello sviluppo mobile: cosa sono, metodi e framework

Autore: IT Sectr Pubblicato: 2026-04-06 Tempo di lettura: 9 min

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

  • Test unitario — verifica di un singolo modulo (funzione, metodo, classe) isolato dalle dipendenze esterne
  • Principi FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — la base dei test di qualità
  • Mock e stub — sostituti delle dipendenze esterne (DB, API, file system) che garantiscono l'isolamento del test
  • TDD (Test-Driven Development) — metodologia di sviluppo guidato dai test: rosso-verde-refactoring
  • Piramide dei test — i test unitari occupano il 70% della piramide, fornendo feedback rapido a ogni commit

Cosa sono i test unitari?

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.

Perché sono necessari i test unitari?

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.

Cosa è considerato un test unitario?

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.

Principi FIRST e struttura AAA

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.

Struttura AAA (Arrange-Act-Assert)

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.

kotlin
// 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)
    }
}

Denominazione dei test

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.

Mock, stub e fake: cosa e quando usare

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).

DoppioCosa verificaEsempio
MockChiamata del metodo con parametri correttiuserRepository.save(user) è stato chiamato esattamente 1 volta
StubValore di ritornorepository.findById(1) restituisce User(id=1, name="Test")
FakeLogica tramite implementazione semplificataInMemoryMapUserRepository con HashMap invece di DB
SpiaMocking parziale di un oggetto realespy(repo).when(findById).thenReturn(user)

Mockito: esempio di mocking in Java/Kotlin

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.

kotlin
// 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: sviluppo guidato dai test

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.

Vantaggi del TDD

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.

Quando TDD non è adatto?

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.

Test unitari nelle applicazioni mobili

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.

Test unitari su Android (JUnit + Mockito/Robolectric)

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.

Test unitari su iOS (XCTest + Quick/Nimble)

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.

Test unitari su Flutter (flutter_test + Mockito)

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.

dart
// 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);
    });
}

Buone pratiche ed errori comuni

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.

  • Una verifica per test — un assert o un gruppo di assert correlati per una verifica logica
  • Evitate la duplicazione — utilizzate @BeforeEach / setUp per l'inizializzazione comune, test parametrizzati per diversi dati di input
  • Non testate i metodi privati — testate attraverso l'API pubblica. Se un metodo privato non è coperto, la sua logica non è visibile esternamente
  • Coprite i casi limite — collezioni vuote, null/undefined, numeri negativi, valori massimi
  • Non usate Thread.sleep nei test — rende i test lenti e instabili. Usate timeout di test e coroutine

Quale copertura è considerata sufficiente?

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%.

CI/CD e test unitari

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

Qual è la differenza tra un test unitario e un test di integrazione?

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%.

Quale framework scegliere per i test unitari?

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.

Quali sono i principi F.I.R.S.T. dei test?

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.

È necessario scrivere test unitari per ViewModel su Android/iOS?

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.

Come testare codice con richieste di rete?

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

  • Test unitari — verifica di moduli individuali isolati dalle dipendenze esterne con feedback rapido
  • Struttura AAA — Arrange (preparazione), Act (azione), Assert (verifica) — modello standard di test
  • Mock e stub — doppi di test per isolamento: i mock verificano le chiamate, gli stub restituiscono valori
  • TDD — sviluppo guidato dai test (Rosso-Verde-Refactor) riduce i difetti del 40-80%
  • Principi FIRST — Fast, Isolated, Repeatable, Self-validating, Timely — base dei test di qualità
  • Strumenti per piattaforma — JUnit 5 (Android), XCTest (iOS), flutter_test (Flutter), Jest (React Native)
  • Copertura 70-80% — livello ottimale per la logica di business critica, getter e setter non richiedono test

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.

Discuti il progetto

Leggi anche