Swift Testing vs XCTest: Guida alla Migrazione con Xcode 26 e Swift 6.2 (2026)

Swift Testing sostituisce XCTest con macro moderne, test parametrizzati nativi e parallelismo di default. Guida pratica alla migrazione con Xcode 26.

Swift Testing vs XCTest: Guida 2026

Aggiornato: 4 luglio 2026

Swift Testing è il nuovo framework di test nativo di Apple, introdotto con Swift 6.0 e completamente integrato in Xcode 26, che sostituisce XCTest con macro moderne come @Test, #expect e @Suite. Migrare da XCTest a Swift Testing significa scrivere meno codice, ottenere messaggi di errore più chiari e sfruttare la concorrenza strutturata di Swift 6.2. In questa guida trovi il confronto completo, esempi runnable e una strategia di migrazione incrementale che ho usato su progetti reali con più di 3.000 test.

  • Swift Testing usa macro (@Test, #expect) invece di sottoclassi di XCTestCase, riducendo il boilerplate di circa il 40%.
  • Xcode 26 esegue XCTest e Swift Testing nello stesso target senza configurazione aggiuntiva, permettendo una migrazione graduale.
  • I test parametrizzati con arguments: sostituiscono i cicli manuali di XCTest e generano un caso di test distinto per ogni input.
  • Swift Testing esegue i test in parallelo di default sfruttando Swift Concurrency, contro l'esecuzione sequenziale storica di XCTest.
  • #expect non interrompe il test in caso di fallimento; usa #require quando devi fermare l'esecuzione (analogo a XCTUnwrap).
  • La migrazione consigliata è progressiva: nuovi test in Swift Testing, riscrittura di quelli esistenti solo dove servono parametrizzazione o tag.

Che cos'è Swift Testing?

Swift Testing è un framework di test open source sviluppato da Apple e rilasciato con Swift 6.0 nel 2024, ora giunto a maturità produttiva in Swift 6.2 e integrato nativamente in Xcode 26. A differenza di XCTest, che risale al 2013 ed è basato su convenzioni ereditate da OCUnit, Swift Testing è pensato dall'inizio per Swift moderno: usa macro anziché runtime introspection, sfrutta async/await in modo nativo e adotta un modello di espressioni testuali più espressivo tramite #expect.

Nella mia esperienza migrando codebase reali, il vantaggio più immediato non è tecnico ma cognitivo: le suite non richiedono più il prefisso test né l'ereditarietà da XCTestCase, e i messaggi di fallimento mostrano il valore effettivo di ogni sotto-espressione. Se scrivi #expect(order.total == expected * quantity), in caso di errore vedi esattamente quanto valeva expected, quanto quantity e quanto order.total, senza dover scrivere label manuali in XCTAssertEqual.

Il framework è distribuito come pacchetto Swift e viene scaricato automaticamente quando aggiungi un target di test in Xcode 26. Per approfondire l'implementazione, la documentazione di riferimento è la documentazione ufficiale Swift Testing di Apple.

Swift Testing vs XCTest: tabella di confronto

Prima di decidere quale usare in un nuovo modulo, conviene guardare le differenze sulle dimensioni che contano davvero: sintassi, discovery, parallelismo, gestione degli errori e integrazione con CI.

CaratteristicaSwift TestingXCTest
Discovery dei testMacro @Test su qualsiasi funzioneMetodi con prefisso test in sottoclassi di XCTestCase
Sintassi di asserzione#expect(...), #require(...)XCTAssertEqual, XCTAssertTrue, ecc.
Test parametrizzatiNativi con arguments:Solo cicli manuali
ParallelismoDefault: parallelo con isolamentoDefault: sequenziale nella stessa classe
Async/awaitSupporto di prima classeSupportato ma con boilerplate
Tag e filtri.tags(.critical), .disabled("motivo")Nessun sistema di tag nativo
Xcode minimoXcode 16 (integrato), Xcode 26 (raccomandato)Xcode 4+
PiattaformeApple, Linux, WindowsPrincipalmente Apple e Linux

La conclusione pratica: Swift Testing è superiore per nuovi test, mentre XCTest resta indispensabile per UI test (XCUIApplication) e test di performance (measure {}), che al momento non hanno equivalenti in Swift Testing. Puoi far coesistere entrambi nello stesso target, ed è esattamente quello che consiglio.

Requisiti: Xcode 26, Swift 6.2 e compatibilità

Swift Testing richiede Swift 6.0 come minimo, ma per sfruttare tutte le funzionalità descritte in questa guida (trait custom, isolamento personalizzato, miglioramenti al parallelismo) servono Swift 6.2 e Xcode 26. Se hai ancora Xcode 15, il framework non è disponibile: dovrai aggiornare l'IDE prima di iniziare la migrazione.

Sui progetti che usano Swift Package Manager, aggiungi la dipendenza in Package.swift per compilazioni da riga di comando o CI che non usano il toolchain Xcode:

// Package.swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MyApp",
    dependencies: [
        // Swift Testing è incluso nel toolchain di Swift 6+;
        // aggiungi il package solo se targettizzi Linux o vuoi versioni specifiche.
        .package(url: "https://github.com/swiftlang/swift-testing.git", from: "6.2.0"),
    ],
    targets: [
        .testTarget(
            name: "MyAppTests",
            dependencies: [
                "MyApp",
                .product(name: "Testing", package: "swift-testing"),
            ]
        )
    ]
)

In Xcode 26 non serve alcuna configurazione: apri lo scheme, seleziona il target di test e importa Testing nel file. La coesistenza con XCTest è automatica.

Scrivere il primo test con la macro @Test

La macro @Test trasforma qualsiasi funzione (globale, di istanza o statica) in un caso di test. Non serve ereditare da nessuna classe, non serve un prefisso specifico, e puoi passare un nome leggibile che verrà mostrato nel navigatore di Xcode.

import Testing
@testable import ShoppingCart

@Test("Il totale del carrello somma prezzo e quantità")
func cartTotalComputesCorrectly() {
    let cart = Cart()
    cart.add(Product(name: "Libro", price: 12.90), quantity: 2)
    cart.add(Product(name: "Penna", price: 1.50), quantity: 3)

    #expect(cart.total == 30.30)
    #expect(cart.itemCount == 5)
}

Confronta con l'equivalente XCTest, che richiede una classe, ereditarietà e chiamate XCTAssertEqual con delta espliciti per i floating point:

import XCTest
@testable import ShoppingCart

final class CartTests: XCTestCase {
    func testCartTotalComputesCorrectly() {
        let cart = Cart()
        cart.add(Product(name: "Libro", price: 12.90), quantity: 2)
        cart.add(Product(name: "Penna", price: 1.50), quantity: 3)

        XCTAssertEqual(cart.total, 30.30, accuracy: 0.001)
        XCTAssertEqual(cart.itemCount, 5)
    }
}

Differenza tra #expect e #require

Una delle domande più frequenti dopo @Test è quando usare #expect e quando #require. La regola è semplice: #expect registra un fallimento ma continua l'esecuzione, mentre #require lancia un errore e ferma il test se la condizione non è vera. Usa #require quando i controlli successivi non hanno senso senza quel valore.

@Test func userProfileHasEmail() throws {
    let profile = try loadProfile(id: "abc-123")

    // Se profile.email è nil, tutti i #expect sotto crashano:
    // usiamo #require per fermarci con un messaggio chiaro.
    let email = try #require(profile.email)

    #expect(email.contains("@"))
    #expect(email.hasSuffix(".it") || email.hasSuffix(".com"))
    #expect(email.count > 5)
}

#require è l'equivalente diretto di XCTUnwrap, ma funziona anche con condizioni booleane e non solo con opzionali. Un pattern che uso spesso: try #require(response.statusCode == 200) per interrompere immediatamente se una chiamata HTTP fallisce, prima di controllare il payload.

Come organizzare i test con @Suite

La macro @Suite raggruppa test correlati in una struct, class o actor. Il vantaggio rispetto a XCTestCase è duplice: puoi usare qualsiasi tipo (non solo classi), e Xcode 26 mostra la gerarchia in modo visivo con supporto per suite annidate.

@Suite("Autenticazione utente")
struct AuthenticationTests {
    let service: AuthService

    // init funziona come setUp: viene eseguito prima di ogni test
    init() {
        service = AuthService(baseURL: URL(string: "https://api.example.it")!)
    }

    @Test func loginConCredenzialiValide() async throws {
        let session = try await service.login(email: "[email protected]", password: "pw")
        #expect(session.isActive)
    }

    @Test func loginConPasswordSbagliata() async throws {
        await #expect(throws: AuthError.invalidCredentials) {
            try await service.login(email: "[email protected]", password: "wrong")
        }
    }

    // Suite annidata per raggruppare i test di logout
    @Suite("Logout")
    struct LogoutTests {
        @Test func logoutInvalidaSessione() async throws {
            // ...
        }
    }
}

Nota che init() sostituisce setUp() e viene eseguito per ogni test, garantendo isolamento senza codice extra. Se hai bisogno di una risorsa condivisa e costosa, usa @Suite(.serialized) per disabilitare il parallelismo su quella suite specifica.

Come eseguire test parametrizzati in Swift Testing

I test parametrizzati sono probabilmente la killer feature di Swift Testing. In XCTest, se volevi testare la stessa logica con dieci input diversi, dovevi scrivere un for loop e perdere la granularità nei report. Con Swift Testing, ogni combinazione diventa un caso di test indipendente, con nome derivato dagli argomenti.

@Test("Validazione codice fiscale", arguments: [
    ("RSSMRA85M01H501U", true),
    ("RSSMRA85M01H501X", false),  // checksum errato
    ("RSSMRA85M01H501",  false),  // troppo corto
    ("",                 false),
])
func validaCodiceFiscale(codice: String, atteso: Bool) {
    #expect(CodiceFiscale.isValid(codice) == atteso)
}

Puoi anche combinare due collezioni con il prodotto cartesiano, utile per test di compatibilità cross-platform:

@Test(arguments: ["it_IT", "en_US", "de_DE"], [Currency.eur, .usd, .gbp])
func formatterFunzionaConTutteLeCombinazioni(locale: String, currency: Currency) {
    let formatter = PriceFormatter(locale: Locale(identifier: locale), currency: currency)
    #expect(!formatter.format(100).isEmpty)
}
// Genera 9 casi di test (3 × 3), ognuno visibile e rieseguibile da solo in Xcode.

Tag, traits e condizioni di esecuzione

I traits sono il meccanismo con cui Swift Testing esprime metadati sui test: tag, condizioni di enable/disable, timeout, restrizioni di piattaforma. Aggiungili come argomenti dopo il nome:

extension Tag {
    @Tag static var critical: Self
    @Tag static var slow: Self
    @Tag static var network: Self
}

@Test(.tags(.critical, .network),
      .enabled(if: ProcessInfo.processInfo.environment["RUN_NETWORK"] == "1"),
      .timeLimit(.minutes(1)))
func downloadConfigurazioneRemota() async throws {
    let config = try await ConfigLoader.fetch()
    #expect(config.version >= 3)
}

Da riga di comando puoi filtrare per tag: swift test --filter tag:critical. Questa è una funzionalità che XCTest non ha mai avuto e che sostituisce i vari hack basati su naming convention (es. testSlow_*) che ho visto usare per anni.

Come migrare da XCTest a Swift Testing passo passo

La migrazione incrementale è la strategia che ha funzionato meglio su tutti i progetti che ho seguito. Ecco i passi che seguo, in ordine:

  1. Aggiorna a Xcode 26 e Swift 6.2 per l'intero team. Non è opzionale, script CI compresi.
  2. Non toccare i test esistenti. XCTest e Swift Testing girano nello stesso target senza conflitti.
  3. Scrivi i nuovi test in Swift Testing, inclusi quelli per feature nuove e bug fix.
  4. Migra prima i test parametrizzati. Sono quelli che beneficiano di più: cicli for di XCTest diventano arguments: con un miglioramento immediato dei report.
  5. Usa uno script di codemod per la conversione meccanica. Puoi trovare esempi di trasformazioni con il repository ufficiale swiftlang/swift-testing su GitHub, che include tabelle di corrispondenza tra le API.
  6. Lascia XCTest per UI test e performance test finché Swift Testing non avrà equivalenti stabili.

Le trasformazioni più comuni sono meccaniche: XCTAssertEqual(a, b)#expect(a == b), XCTAssertTrue(x)#expect(x), XCTAssertNil(x)#expect(x == nil), XCTUnwrap(x)try #require(x). Un pattern non ovvio: XCTAssertThrowsError con controllo del tipo di errore diventa #expect(throws: MyError.specific), che è molto più espressivo.

Concorrenza, async/await e isolamento

Swift Testing esegue i test in parallelo di default, in linea con il modello di SwiftData in iOS 26 e la sua ereditarietà dei modelli che assume concorrenza strutturata. Ogni @Test gira in un Task separato; se hai stato condiviso (database in memoria, singleton), devi gestirlo esplicitamente.

@Suite("Persistenza CoreData", .serialized)
struct PersistenceTests {
    // .serialized garantisce che questi test non girino in parallelo,
    // utile se condividono un container CoreData in memoria.
}

Per test isolati su un actor specifico (per esempio @MainActor), puoi usare l'attributo direttamente sul test:

@Test @MainActor
func aggiornaUIDopoNotifica() async {
    let viewModel = HomeViewModel()
    viewModel.receive(.newMessage)
    #expect(viewModel.state == .refreshed)
}

Se stai integrando questi test con codice che usa Combine o pipeline asincrone, dai un'occhiata alla nostra guida su Foundation Models e IA on-device con Swift, che copre pattern di test asincroni analoghi per pipeline di inferenza.

Errori comuni e come evitarli

Ecco gli errori che vedo più spesso nei code review dopo una migrazione, con le relative soluzioni.

Dimenticare try davanti a #require

#require lancia errori: senza try, il compilatore protesta e il messaggio non è sempre chiarissimo. Ricorda che ogni funzione di test che usa #require deve essere marcata throws.

Aspettarsi setUp come metodo

Non c'è più setUp() o tearDown(). Usa init() e deinit della struct/class. Se hai bisogno di setup async, aggiungi init() async throws.

Presumere che i test girino in ordine

Con il parallelismo di default, non puoi assumere ordine. Se un test dipende dallo stato lasciato da un altro, il codice è fragile a prescindere dal framework — riscrivilo per essere idempotente.

Non aggiornare i pipeline CI

Se usi GitHub Actions o Xcode Cloud, verifica che i runner usino l'immagine con Xcode 26. Altrimenti i test annotati con @Test non verranno neanche scoperti e il report mostrerà zero test in errore — silenzioso e pericoloso. Per confronto tra strumenti CI moderni consulta la nostra guida ad App Intents in iOS 26, che include note sui runner necessari.

Domande frequenti

Swift Testing sostituisce completamente XCTest?

No. Swift Testing sostituisce i test unitari e di integrazione, ma XCTest resta necessario per UI test tramite XCUIApplication e per test di performance con measure {}. Apple ha confermato che i due framework coesisteranno a lungo termine.

Posso usare Swift Testing con Xcode 15?

No. Swift Testing richiede almeno Xcode 16 con Swift 6.0. Per le funzionalità descritte in questa guida (trait custom, parallelismo migliorato, isolamento personalizzato) servono Xcode 26 e Swift 6.2.

Come si esegue un singolo test da riga di comando?

Con swift test --filter NomeSuite/nomeTest oppure per tag con swift test --filter tag:critical. In Xcode 26 puoi anche cliccare l'icona a lato del test nel navigatore, e per i test parametrizzati puoi rieseguire un singolo caso.

Qual è la differenza tra @Test e @Suite?

@Test marca una singola funzione come caso di test. @Suite raggruppa più test in un tipo (struct, class o actor) permettendo di condividere setup nell'init(), applicare traits comuni e creare gerarchie annidate. Le suite sono facoltative ma consigliate per organizzare progetti medi e grandi.

Swift Testing supporta i mock?

Il framework non include un mocking system, ma è progettato per lavorare con qualsiasi libreria esistente (Mockingbird, Cuckoo, o mock manuali basati su protocolli). Per la maggior parte dei casi, i mock protocollari scritti a mano sono la scelta più idiomatica in Swift 6.2.

Editorial Team
Sull'Autore Editorial Team

Our team of expert writers and editors.