Per fare in modo che i casi di test XCTest superino sia l’esecuzione individuale sia quella dell’intera suite, assegna a ogni test una propria fixture modificabile e iniettala nel codice da testare. Quando un singleton deve rimanere condiviso, reimpostalo prima di ogni test e impedisci accessi sovrapposti per tutta la durata del test.

Questa guida segue un’attività di sviluppo su Mac: correggere in Xcode dei test di calcolo del layout che passano da soli ma falliscono quando vengono eseguiti insieme. L’esempio usa solo larghezze intere sintetiche e calcoli locali. Non richiede finestre dell’app, mock di servizi, archiviazione di file o connessioni di rete.

Perché un test può passare da solo e fallire nella suite?

Un test eseguito in isolamento può trovare il singleton nello stato iniziale. In una suite, un test precedente potrebbe averlo già modificato. Un nuovo riferimento a quel singleton punta comunque allo stesso oggetto modificabile.

Swift distingue le proprietà di istanza dalle proprietà di tipo: ogni istanza ha le proprie proprietà di istanza, mentre una proprietà di tipo ha una sola copia condivisa. Le proprietà di tipo memorizzate vengono inizializzate al primo accesso e solo una volta. Di conseguenza, assegnare SpacingCalculator.shared a una proprietà del test non crea un nuovo calcolatore. Consulta la documentazione sulle proprietà del progetto Swift.

Considera una riga sintetica contenente elementi con larghezze [10, 20, 30]. La larghezza totale è la somma delle larghezze degli elementi più gli spazi tra di essi. Con uno spazio predefinito di 2, il risultato è 64; con uno spazio di 8, è 76.

Supponiamo che un test verifichi la larghezza predefinita senza configurare il calcolatore, mentre un altro imposti lo spazio condiviso su 8. La tabella seguente descrive l’esecuzione sequenziale, iniziando ogni riga con un nuovo calcolatore il cui spazio è 2. Sono calcoli illustrativi, non risultati registrati di test Xcode.

Esecuzione Calcolatore condiviso senza reimpostazione Calcolatore nuovo per ogni test
Test predefinito da solo 64: passa 64: passa
Test con spazio ampio da solo 76: passa 76: passa
Predefinito, poi spazio ampio Entrambi passano Entrambi passano
Spazio ampio, poi predefinito Il test predefinito riceve 76: fallisce Entrambi passano

Questo offre un’indicazione utile per la diagnosi: individua il test che modifica lo stato usato dall’asserzione che fallisce. Cambiare i nomi dei test per ottenere un ordine favorevole lascia intatta la dipendenza.

Quale metodo del ciclo di vita XCTest dovrebbe gestire la fixture?

Usa un metodo di setup dell’istanza per lo stato modificabile necessario a ogni test. Apple documenta che il setup dell’istanza viene eseguito prima di ogni metodo di test, mentre il setup a livello di classe viene eseguito una volta sola prima della classe di test. Una fixture modificabile creata nel setup a livello di classe non riceve quindi una nuova condizione iniziale tra un metodo e l’altro. Consulta Impostare e smantellare lo stato nei test.

Scegli il metodo di setup adatto all’operazione: la creazione sincrona e senza eccezioni della fixture si adatta a setUp(); la creazione che può generare eccezioni si adatta a setUpWithError(); la preparazione asincrona si adatta a setUp() async throws. Usa il teardown per completare la pulizia del test corrente. Apple sconsiglia esplicitamente di affidarsi al teardown per preparare i test successivi, perché un arresto anomalo può impedirne l’esecuzione. La stessa documentazione sul ciclo di vita descrive questi limiti.

La scelta della fixture dipende sia dalla proprietà sia dall’ambito di esecuzione:

Situazione Approccio appropriato Condizione da verificare
I test possono creare la dipendenza Crea e inietta un’istanza per ogni test Il consumer deve usare quell’istanza iniettata
I test devono esercitare il singleton effettivo Ripristina una condizione iniziale completa prima di ogni test Nessun test sovrapposto deve modificare quel singleton
La configurazione condivisa è immutabile Riutilizza il valore immutabile Anche gli oggetti a cui fa riferimento devono rimanere invariati
Un test avvia un’attività asincrona Attendi il completamento prima di terminare il test Nessuna attività incompleta deve poter modificare la fixture in seguito

Per questo calcolatore, la prima opzione è la più semplice. Il codice di produzione può mantenere un valore predefinito condiviso, mentre i test scelgono esplicitamente istanze indipendenti.

Crea una fixture XCTest con proprietà indipendente

Usa un Mac con Xcode e un target di unit test macOS esistente. Aggiungi il seguente esempio completo come singolo file Swift a quel target di test. Definisce il proprio calcolatore, consumer e test, quindi non richiede l’importazione di alcun modulo dell’applicazione. Usa una toolchain Swift 6 con l’isolamento actor predefinito del target impostato su nonisolated.

Il calcolatore è un actor per rendere esplicito il confine di concorrenza del suo stato modificabile. Gli actor Swift limitano l’accesso al proprio stato modificabile e le chiamate dall’esterno dell’actor usano await. Queste regole sono documentate nel capitolo sulla concorrenza del progetto Swift. Anche con un actor, è necessario che le fixture abbiano proprietà indipendente.

import XCTest

actor SpacingCalculator {
    static let shared = SpacingCalculator()

    private var gap = 2

    func setGap(_ value: Int) {
        precondition(value >= 0)
        gap = value
    }

    func width(of itemWidths: [Int]) -> Int {
        precondition(itemWidths.allSatisfy { $0 >= 0 })
        let gapCount = max(itemWidths.count - 1, 0)
        return itemWidths.reduce(0, +) + gapCount * gap
    }
}

struct RowLayout {
    let calculator: SpacingCalculator

    init(calculator: SpacingCalculator = .shared) {
        self.calculator = calculator
    }

    func width(of itemWidths: [Int]) async -> Int {
        await calculator.width(of: itemWidths)
    }
}

final class RowLayoutTests: XCTestCase {
    private var calculator: SpacingCalculator?

    override func setUp() {
        super.setUp()
        calculator = SpacingCalculator()
    }

    override func tearDown() {
        calculator = nil
        super.tearDown()
    }

    func testDefaultGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 64)
    }

    func testWideGap() async throws {
        let fixture = try XCTUnwrap(calculator)
        await fixture.setGap(8)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10, 20, 30])

        XCTAssertEqual(actual, 76)
    }

    func testEmptyRow() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [])

        XCTAssertEqual(actual, 0)
    }

    func testSingleItem() async throws {
        let fixture = try XCTUnwrap(calculator)
        let layout = RowLayout(calculator: fixture)

        let actual = await layout.width(of: [10])

        XCTAssertEqual(actual, 10)
    }
}

La riga decisiva è calculator = SpacingCalculator(). Ogni setup crea un actor indipendente con uno spazio di 2. Ogni test passa quindi quell’actor a RowLayout. Chiamare RowLayout() all’interno di questi test selezionerebbe .shared, aggirando la fixture.

L’esempio consente deliberatamente la creazione diretta. Se il singleton esistente ha un inizializzatore privato, introduci un percorso di creazione interno oppure estrai il suo stato di calcolo modificabile in una dipendenza che possa essere creata separatamente. Un inizializzatore interno può essere reso accessibile a un target di test tramite la configurazione @testable import già adottata dal modulo dell’applicazione; non è necessario rendere sostituibile a livello globale il riferimento al singleton.

Il calcolo definisce anche due casi limite utili: una riga vuota ha larghezza 0 e un singolo elemento non presenta spazi intermedi. L’esempio accetta interi non negativi abbastanza piccoli da evitare l’overflow; i valori negativi violano le precondizioni e terminano l’esecuzione invece di produrre un fallimento dell’asserzione.

I calcoli attesi e i due ordini possibili dei test sono stati verificati localmente con calcoli Python deterministici. Gli esempi Swift sono stati esaminati, ma non compilati né eseguiti perché questo ambiente di ricerca non dispone di una toolchain Swift. Eseguili in Xcode per verificare la compilazione e l’esecuzione nel tuo target.

Quando è sufficiente reimpostare il singleton?

La reimpostazione è un approccio transitorio praticabile quando i test devono davvero usare l’istanza condivisa e tutto il suo stato visibile ai test può essere ripristinato. Imposta la condizione iniziale prima di ogni test, compresi i valori che un test presume siano quelli predefiniti. Reimpostare solo la proprietà modificata dal test che attualmente fallisce può lasciare inosservate altre dipendenze.

Per questo calcolatore, il setup asincrono potrebbe selezionare SpacingCalculator.shared e attendere setGap(2) prima di ogni metodo. In questo modo si ripristina la condizione iniziale del calcolo solo se nessun chiamante sovrapposto modifica lo spazio prima dell’asserzione. Se in seguito la fixture include risultati memorizzati nella cache o altre configurazioni, l’operazione di ripristino dovrà coprire anche quei valori.

Un actor non assegna a un test la proprietà esclusiva dell’istanza attraverso più chiamate con await. Per esempio, il test A può impostare lo spazio condiviso su 2, il test B può impostarlo su 8 e il test A può poi calcolare una larghezza usando 8. Ogni operazione dell’actor può essere eseguita correttamente, mentre l’ipotesi complessiva del test non viene rispettata. Questo deriva dalle regole documentate di esecuzione e sospensione degli actor di Swift.

Prima di scegliere l’isolamento tramite reimpostazione, individua tutti i test e le attività in background che possono accedere all’oggetto condiviso. Se non puoi garantirne l’uso esclusivo durante la preparazione, l’esecuzione e la pulizia, inietta invece uno stato indipendente. Ripristinare uno snapshot in seguito non può impedire a un altro chiamante di osservare le modifiche temporanee durante il test.

Come si traduce la fixture in Swift Testing?

Swift Testing inizializza un’istanza distinta della suite per ogni funzione di test di istanza. Una proprietà di istanza che crea un calcolatore offre quindi lo stesso modello di proprietà senza dover ridefinire i metodi di setup di XCTest. Una proprietà inizializzata da .shared continuerebbe invece a riferirsi al singleton. Apple documenta il ciclo di vita delle istanze in Organizzare le funzioni di test con i tipi suite.

Con le definizioni precedenti di SpacingCalculator e RowLayout presenti una volta sola nello stesso target di test, questa è una suite aggiuntiva completa per Swift Testing. Richiede una toolchain che includa il modulo Testing, come Xcode 16 o successivo. Anche il suo isolamento actor predefinito dovrebbe essere nonisolated.

import Testing

struct RowLayoutSwiftTests {
    let calculator = SpacingCalculator()

    @Test func defaultGap() async {
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 64)
    }

    @Test func wideGap() async {
        await calculator.setGap(8)
        let layout = RowLayout(calculator: calculator)
        let actual = await layout.width(of: [10, 20, 30])
        #expect(actual == 76)
    }
}

Per una preparazione più articolata, Swift Testing supporta gli inizializzatori di suite, inclusi quelli asincroni o che possono generare eccezioni. La guida Apple Migrare un test da XCTest illustra questa alternativa ai metodi di setup di XCTest. Questa fixture non richiede una pulizia esplicita perché possiede solo stato di calcolo locale e non avvia attività in background.

Swift Testing esegue i test in parallelo per impostazione predefinita. Applicando .serialized a una suite, i test contenuti e le suite annidate vengono eseguiti in sequenza, ma ciò non impedisce l’esecuzione contemporanea di test non correlati. Applicare il trait a una normale funzione di test non parametrizzata non produce alcun effetto. Questi limiti sono esplicitati in Eseguire test in sequenza o in parallelo.

Di conseguenza, collocare due suite dipendenti dallo stesso singleton sotto annotazioni .serialized separate non ne garantisce l’esclusione reciproca. Un genitore comune serializzato può coordinare i suoi discendenti, a condizione che tutti gli utenti concorrenti appartengano a quell’ambito. Le fixture nuove evitano questa necessità di coordinamento per questi test di calcolo.

Verifica in Xcode i risultati individuali e quelli della suite

Durante tutta la verifica, usa lo stesso schema, lo stesso piano di test e la stessa destinazione Mac. Apple documenta l’esecuzione di singoli metodi, suite e del piano di test attivo in Eseguire i test e interpretarne i risultati.

  1. Esegui individualmente ciascuno dei quattro metodi XCTest usando il pulsante di riproduzione nel margine dell’editor. Aspettati 64, 76, 0 e 10, rispettivamente.
  2. Esegui la classe RowLayoutTests usando il pulsante di riproduzione accanto alla sua dichiarazione. Tutti e quattro i metodi dovrebbero passare insieme.
  3. Scegli Product > Test per eseguire il piano di test attivo. Verifica che questi test siano stati effettivamente eseguiti e non saltati o esclusi.
  4. Se hai aggiunto l’esempio Swift Testing, esegui anche individualmente e insieme le sue due funzioni.

Se i test individuali passano ma l’intero piano fallisce, segui il percorso dell’oggetto usato dal consumer che fallisce. Cerca un’iniezione mancante, un helper che chiama .shared o un’attività asincrona che continua dopo il test che l’ha avviata. Se un test fallisce da solo, controlla i suoi input e il calcolo atteso prima di attribuire il problema all’ordine della suite.

La fixture completata dovrebbe rendere visibile lo stato iniziale nel percorso di creazione del test corrente. Per questi calcoli di layout, ogni test possiede un calcolatore, lo fornisce esplicitamente al consumer e ne attende il completamento delle operazioni. Il risultato atteso diventa così indipendente dall’ordine in cui sono stati eseguiti i test vicini.

Fonti