Swift Testing : le guide complet du framework de tests moderne pour Swift

Découvrez Swift Testing, le nouveau framework de tests d'Apple conçu pour Swift. Ce guide couvre tout : @Test, #expect, #require, tests paramétrés, suites, tags, exit tests et attachments de Swift 6.2, plus une stratégie concrète de migration depuis XCTest.

Introduction : pourquoi Swift Testing change la donne

Si vous développez en Swift depuis quelques années, vous avez sans doute une relation… compliquée avec XCTest. Soyons honnêtes : ce framework, hérité d'Objective-C, a été le compagnon fidèle — mais souvent frustrant — de tous les développeurs iOS. Entre ses quarante variantes de XCTAssert, ses classes obligatoires héritant de XCTestCase et ses conventions de nommage rigides, XCTest montrait clairement ses limites face aux évolutions du langage Swift.

C'est dans ce contexte qu'Apple a présenté Swift Testing à la WWDC 2024 — un framework de tests entièrement repensé, conçu spécifiquement pour Swift. Et franchement, après quelques mois d'utilisation, c'est difficile de revenir en arrière. Avec Swift 6.2 (sorti en septembre 2025), le framework a encore gagné en maturité grâce à l'ajout des exit tests, des attachments et des confirmations étendues.

Dans ce guide, on va explorer Swift Testing en profondeur : des fondamentaux aux fonctionnalités avancées, en passant par les bonnes pratiques d'organisation et les stratégies de migration depuis XCTest. Si vous avez déjà lu nos articles sur Approachable Concurrency et Span dans Swift 6.2, considérez ce guide comme le volet « qualité et tests » de l'écosystème moderne Swift.

Allez, on plonge dedans.

Les fondamentaux : @Test, #expect et #require

Le macro @Test : la simplicité avant tout

Avec Swift Testing, fini les conventions rigides de XCTest. Plus besoin de préfixer vos fonctions de test par test, plus besoin d'hériter de XCTestCase. Une simple annotation @Test suffit :

import Testing

@Test("Vérifie que l'addition fonctionne correctement")
func additionBasique() {
    let resultat = 2 + 3
    #expect(resultat == 5)
}

C'est tout. Pas de classe, pas de préfixe obligatoire, pas de cérémonie. La chaîne de caractères passée à @Test est un nom descriptif qui apparaîtra dans le Test Navigator de Xcode — et c'est quand même beaucoup plus lisible qu'un nom de fonction du genre testThatAdditionOfTwoPositiveNumbersReturnsCorrectResult, non ?

La fonction de test peut être une fonction libre, une méthode d'instance, ou une méthode statique d'un type. Et ce type peut être une structure, une classe ou même un acteur — Swift Testing ne vous impose aucune contrainte sur ce point.

#expect : l'assertion souple

Le macro #expect est l'outil d'assertion principal de Swift Testing. Il remplace à lui seul toutes les variantes de XCTAssert que vous connaissiez. Comment ? En acceptant simplement une expression booléenne standard de Swift :

@Test("Vérifie le calcul du prix total")
func calculPrixTotal() {
    let panier = Panier()
    panier.ajouter(Article(nom: "Clavier", prix: 89.99))
    panier.ajouter(Article(nom: "Souris", prix: 49.99))

    #expect(panier.total == 139.98)
    #expect(panier.nombreArticles == 2)
    #expect(!panier.estVide)
}

Mais là où #expect devient vraiment puissant, c'est dans sa capacité à capturer les valeurs évaluées. En cas d'échec, au lieu d'un message cryptique, vous obtenez un diagnostic précis :

// En cas d'échec, Swift Testing affiche :
// Expectation failed: (panier.total → 139.97) == 139.98

Notez que #expect est une assertion souple : si elle échoue, le test continue à s'exécuter. Ça permet de collecter toutes les erreurs d'un test en une seule exécution, plutôt que de s'arrêter au premier problème. Un vrai gain de temps au quotidien.

@Test("Vérifie plusieurs propriétés de l'utilisateur")
func propriétésUtilisateur() {
    let utilisateur = Utilisateur(nom: "Alice", age: 28)

    #expect(utilisateur.nom == "Alice")        // ✅ Réussit
    #expect(utilisateur.age == 30)             // ❌ Échoue, mais continue
    #expect(utilisateur.estMajeur)             // ✅ Réussit
    #expect(utilisateur.email != nil)          // ❌ Échoue aussi
    // Le rapport final montrera 2 échecs sur 4 assertions
}

#require : l'assertion stricte

Parfois, un échec rend la suite du test complètement inutile. C'est exactement le rôle de #require — une assertion stricte qui stoppe immédiatement le test en cas d'échec :

@Test("Charge et vérifie les données utilisateur")
func chargementDonnées() throws {
    let données = try #require(DataLoader.charger("utilisateurs.json"))
    // Si le chargement échoue, le test s'arrête ici immédiatement

    let utilisateurs = try #require(données.décoder([Utilisateur].self))
    // Si le décodage échoue, pas la peine de continuer

    #expect(utilisateurs.count > 0)
    #expect(utilisateurs.first?.nom == "Alice")
}

Le mot-clé try est obligatoire avec #require, car le mécanisme sous-jacent lance une erreur pour interrompre l'exécution. C'est aussi le remplacement moderne de XCTUnwrap pour le déballage sûr des optionnels :

@Test("Vérifie le premier élément de la liste")
func premierÉlément() throws {
    let liste = [1, 2, 3]
    let premier = try #require(liste.first)
    // Si la liste est vide, le test s'arrête avec un message clair
    #expect(premier == 1)
}

Quand utiliser #expect vs #require ?

La règle est simple : utilisez #require quand l'échec rend la suite du test impossible ou dénuée de sens (déballage d'optionnels, chargement de ressources, préconditions). Utilisez #expect pour toutes les vérifications qui peuvent échouer indépendamment les unes des autres. Dans le doute, préférez #expect — vous aurez plus d'informations en cas d'échec.

Tests paramétrés : écrire moins, tester plus

Les tests paramétrés sont probablement la fonctionnalité la plus enthousiasmante de Swift Testing par rapport à XCTest. Et honnêtement, une fois qu'on y a goûté, on ne revient plus en arrière. Au lieu d'écrire dix fonctions de test quasi identiques, vous en écrivez une seule qui s'exécute sur une collection de valeurs.

Paramétrage simple

@Test("Vérifie que les nombres pairs sont détectés", arguments: [2, 4, 8, 16, 100])
func nombrePair(valeur: Int) {
    #expect(valeur.isMultiple(of: 2))
}

Swift Testing exécute ce test une fois pour chaque valeur de la collection. Dans le Test Navigator de Xcode, chaque argument apparaît comme un cas de test distinct. Si le test échoue pour la valeur 100 mais réussit pour les autres, vous le verrez immédiatement — fini les boucles for obscures dans XCTest où il fallait deviner quel élément avait causé l'échec.

Paramétrage avec des enums

Les tests paramétrés brillent particulièrement avec les enums. Vous pouvez tester tous les cas d'un seul coup :

enum Devise: String, CaseIterable {
    case eur, usd, gbp, jpy
}

@Test("Chaque devise a un symbole valide", arguments: Devise.allCases)
func symboleDevise(devise: Devise) {
    let formateur = FormateurMonétaire(devise: devise)
    let symbole = formateur.symbole
    #expect(!symbole.isEmpty)
    #expect(symbole.count <= 3)
}

Produit cartésien avec deux arguments

Quand vous fournissez deux collections d'arguments, Swift Testing teste toutes les combinaisons possibles (le fameux produit cartésien) :

@Test("Addition fonctionne pour toutes les combinaisons",
       arguments: [1, 5, 10], [2, 3, 7])
func addition(a: Int, b: Int) {
    let calculatrice = Calculatrice()
    #expect(calculatrice.additionner(a, b) == a + b)
}
// Exécute 9 tests : (1,2), (1,3), (1,7), (5,2), (5,3), (5,7), (10,2), (10,3), (10,7)

Neuf tests générés automatiquement à partir de trois lignes. Difficile de faire plus efficace.

Appariement avec zip()

Si vous ne voulez pas le produit cartésien mais un appariement un-à-un, utilisez zip() :

@Test("Vérifie les codes HTTP attendus",
       arguments: zip(
           ["/api/users", "/api/products", "/api/invalid"],
           [200, 200, 404]
       ))
func codeHTTP(endpoint: String, codeAttendu: Int) async throws {
    let réponse = try await APIClient.get(endpoint)
    #expect(réponse.statusCode == codeAttendu)
}

Ici, chaque endpoint est testé avec son code attendu spécifique, pas avec toutes les combinaisons possibles. Trois tests au lieu de neuf — c'est souvent ce qu'on veut en pratique.

Suites et tags : organiser vos tests à grande échelle

Le macro @Suite

Pour regrouper des tests liés, Swift Testing propose le macro @Suite. Et contrairement à XCTestCase, une suite peut être un struct, une classe ou un acteur :

@Suite("Tests du moteur de recherche")
struct MoteurRechercheTests {
    let moteur = MoteurRecherche()

    @Test("Recherche par mot-clé retourne des résultats")
    func rechercheParMotClé() async throws {
        let résultats = try await moteur.chercher("SwiftUI")
        #expect(!résultats.isEmpty)
    }

    @Test("Recherche vide retourne un tableau vide")
    func rechercheVide() async throws {
        let résultats = try await moteur.chercher("")
        #expect(résultats.isEmpty)
    }
}

Apple recommande d'utiliser des structures plutôt que des classes, car le compilateur Swift peut mieux appliquer les règles de sécurité de la concurrence. Chaque test reçoit sa propre instance de la structure — l'état n'est jamais partagé entre les tests (et c'est tant mieux).

Suites imbriquées

L'un des avantages majeurs par rapport à XCTest, c'est la possibilité d'imbriquer des suites pour créer une vraie hiérarchie de tests :

@Suite("Authentification")
struct AuthTests {

    @Suite("Connexion")
    struct ConnexionTests {
        @Test("Connexion avec identifiants valides")
        func connexionValide() async throws {
            let résultat = try await Auth.connecter(email: "[email protected]", motDePasse: "secret123")
            #expect(résultat.estConnecté)
        }

        @Test("Connexion avec mot de passe erroné échoue")
        func motDePasseErroné() async throws {
            await #expect(throws: AuthError.identifiantsInvalides) {
                try await Auth.connecter(email: "[email protected]", motDePasse: "mauvais")
            }
        }
    }

    @Suite("Inscription")
    struct InscriptionTests {
        @Test("Inscription avec email valide")
        func inscriptionValide() async throws {
            let utilisateur = try await Auth.inscrire(email: "[email protected]", motDePasse: "Fort!123")
            #expect(utilisateur.email == "[email protected]")
        }
    }
}

Dans le Test Navigator de Xcode, cette hiérarchie est parfaitement reflétée. Quand vous avez des centaines de tests, ce genre d'organisation fait vraiment la différence.

Les tags : organisation sémantique transversale

Les tags permettent de catégoriser vos tests selon des axes transversaux — indépendamment de leur position dans la hiérarchie de suites. Un test peut appartenir à une suite et porter plusieurs tags en même temps :

extension Tag {
    @Tag static var critique: Self
    @Tag static var lent: Self
    @Tag static var réseau: Self
    @Tag static var baseDeDonnées: Self
}

@Suite("Synchronisation des données")
struct SyncTests {

    @Test("Synchronise les articles depuis le serveur", .tags(.réseau, .lent))
    func syncArticles() async throws {
        let sync = SyncManager()
        try await sync.synchroniser()
        #expect(sync.articlesLocaux.count > 0)
    }

    @Test("Sauvegarde les articles localement", .tags(.baseDeDonnées, .critique))
    func sauvegardeLocale() throws {
        let store = ArticleStore()
        try store.sauvegarder(articles: [Article.exemple])
        #expect(store.count == 1)
    }
}

Grâce aux tags, vous pouvez exécuter sélectivement vos tests dans Xcode : tous les tests critiques, tous les tests réseau, etc. Les tags sont aussi hérités — si vous appliquez un tag à une suite, tous les tests qu'elle contient en héritent automatiquement. Plutôt malin comme système.

Traits avancés : contrôler le comportement des tests

Sérialisation avec .serialized

Par défaut, Swift Testing exécute les tests en parallèle grâce à Swift Concurrency. C'est excellent pour la performance, mais certains tests accèdent à des ressources partagées qui ne supportent pas les accès concurrents. Le trait .serialized force l'exécution séquentielle :

@Suite("Tests de base de données", .serialized)
struct BaseDeDonnéesTests {

    @Test("Insertion d'un enregistrement")
    func insertion() async throws {
        try await db.insérer(Utilisateur(nom: "Alice"))
        let count = try await db.compter(Utilisateur.self)
        #expect(count == 1)
    }

    @Test("Suppression d'un enregistrement")
    func suppression() async throws {
        try await db.supprimer(where: \.nom == "Alice")
        let count = try await db.compter(Utilisateur.self)
        #expect(count == 0)
    }
}

Attention cependant : n'abusez pas de .serialized. Chaque suite sérialisée ralentit votre suite de tests globale. Préférez isoler l'état partagé quand c'est possible — vos CI vous remercieront.

Désactiver un test avec .disabled

@Test("Fonctionnalité en cours de développement", .disabled("En attente de l'API v3"))
func fonctionnalitéFuture() {
    // Ce test sera marqué comme « ignoré » dans Xcode
}

Court et efficace. Le message explique pourquoi le test est désactivé, ce qui évite les « mais pourquoi ce test est commenté ? » trois mois plus tard.

Conditions d'exécution avec .enabled(if:)

@Test("Test spécifique à macOS", .enabled(if: ProcessInfo.processInfo.isMacCatalystApp == false))
func fonctionnalitéMacOS() {
    // Ne s'exécute que sur macOS natif
}

Lier un test à un bug avec .bug

@Test("Corrige le crash lors du tri", .bug("https://github.com/projet/issues/42", "Crash sur tableau vide"))
func triTableauVide() {
    let tableau: [Int] = []
    let résultat = tableau.trié()
    #expect(résultat.isEmpty)
}

C'est un détail, mais pouvoir lier directement un test à un ticket de bug améliore énormément la traçabilité dans un projet d'équipe.

Gérer les problèmes connus avec withKnownIssue

Quand un test échoue à cause d'un bug connu que vous ne pouvez pas corriger immédiatement, plutôt que de le désactiver (et de l'oublier pendant des mois…), utilisez withKnownIssue :

@Test("Calcul de la moyenne avec nombres négatifs")
func moyenneNégatifs() {
    withKnownIssue("Bug #87 : arrondi incorrect pour les négatifs") {
        let résultat = Statistiques.moyenne([-1.5, -2.5, -3.0])
        #expect(résultat == -2.333, accuracy: 0.001)
    }
}

Le test s'exécute normalement, mais son échec est enregistré comme un « problème connu » plutôt qu'un échec de test. Et quand le bug sera corrigé, le test redeviendra vert automatiquement — Swift Testing vous avertira même que le withKnownIssue n'est plus nécessaire. Plutôt élégant.

Confirmations : tester le code asynchrone

Swift Testing intègre nativement le support de Swift Concurrency avec async/await. Mais pour tester des événements asynchrones basés sur des callbacks (et il y en a encore beaucoup dans les SDK Apple), le framework propose les confirmations :

@Test("L'événement de vente est déclenché")
func événementVente() async {
    await confirmation("La vente est confirmée") { confirmer in
        let boutique = Boutique()
        boutique.gestionnaireDÉvénement = { événement in
            if case .venteConcluée = événement {
                confirmer()
            }
        }
        await boutique.vendre(Article(nom: "Widget"))
    }
}

La confirmation fonctionne de manière similaire à XCTestExpectation, mais sans bloquer le thread appelant. Le test échouera si confirmer() n'est pas appelé avant la fin du bloc.

expectedCount : vérifier le nombre d'appels

@Test("Le gestionnaire d'erreurs n'est jamais appelé")
func aucuneErreur() async {
    await confirmation(expectedCount: 0) { confirmer in
        let processeur = Processeur()
        processeur.onErreur = { _ in confirmer() }
        await processeur.traiter([1, 2, 3])
    }
}

Tester qu'un callback n'est jamais appelé — c'était toujours un casse-tête avec XCTest. Ici, c'est une ligne.

Confirmations avec plages (Swift 6.1+)

Depuis Swift 6.1, vous pouvez utiliser des plages pour les confirmations, ce qui est idéal quand le comportement n'est pas totalement déterministe :

@Test("Entre 3 et 10 notifications sont envoyées")
func notificationsMultiples() async {
    await confirmation(expectedCount: 3...10) { confirmer in
        let notificateur = Notificateur()
        notificateur.onNotification = { _ in confirmer() }
        await notificateur.envoyerRappels()
    }
}

@Test("Au moins une mise à jour est reçue")
func miseÀJourMinimale() async {
    await confirmation(expectedCount: 1...) { confirmer in
        let flux = FluxDeDonnées()
        flux.onMiseÀJour = { confirmer() }
        await flux.démarrer()
    }
}

Les plages partielles ouvertes vers le haut (5...) sont autorisées, mais les plages partielles vers le bas (...10) sont explicitement interdites pour éviter toute ambiguïté.

Nouveautés Swift 6.2 : exit tests et attachments

Exit tests : tester les crashs intentionnels

Bon, parlons de ce qui était probablement la lacune la plus frustrante de XCTest : l'impossibilité de tester du code qui appelle fatalError() ou preconditionFailure(). Swift 6.2 comble enfin cette lacune avec les exit tests :

@Test("Un indice hors limites provoque un crash")
func indiceHorsLimites() async {
    await #expect(processExitsWith: .failure) {
        let tableau = [1, 2, 3]
        let _ = tableau[10] // Doit provoquer un crash
    }
}

En coulisses, Swift Testing lance un processus séparé pour exécuter le code incriminé. Le test est suspendu (d'où le await) jusqu'à ce que le processus se termine, puis le résultat est évalué. Vous pouvez vérifier différentes conditions de sortie :

// Vérifie un échec générique (crash, fatalError, etc.)
await #expect(processExitsWith: .failure) { /* ... */ }

// Vérifie un succès (code de sortie 0)
await #expect(processExitsWith: .success) { /* ... */ }

// Vérifie un code de sortie spécifique
await #expect(processExitsWith: .exitCode(42)) { /* ... */ }

// Vérifie un signal spécifique
await #expect(processExitsWith: .signal(SIGABRT)) { /* ... */ }

Limitation importante : les exit tests ne sont actuellement disponibles que sur macOS, Linux, FreeBSD, OpenBSD et Windows. Le support d'iOS et des autres plateformes Apple est prévu mais n'est pas encore implémenté dans Swift 6.2 — l'approche technique serait apparemment différente sur ces plateformes.

Attachments : enrichir vos rapports de test

Les attachments (pièces jointes) permettent de joindre des données à vos tests pour faciliter le diagnostic en cas d'échec. Si vous avez déjà passé une heure à essayer de reproduire un test intermittent, vous allez adorer cette fonctionnalité :

@Test("Analyse du fichier JSON")
func analyseJSON() throws {
    let json = """
    {"utilisateurs": [{"nom": "Alice"}, {"nom": "Bob"}]}
    """
    let données = Data(json.utf8)

    // Attache les données brutes au test pour le diagnostic
    Attachment.record(données, named: "données-entrée.json")

    let résultat = try JSONAnalyseur.analyser(données)
    #expect(résultat.utilisateurs.count == 2)
}

Swift Testing prend en charge nativement les types String, Data et tout type conforme à Encodable. Les attachments apparaissent dans les résultats de test de Xcode, ce qui est extrêmement utile pour le débogage.

À noter que le support des images (via CGImage ou UIImage) n'est pas encore disponible dans Swift 6.2, mais une proposition est actuellement en discussion sur les forums Swift.

Migration depuis XCTest : stratégie et conseils pratiques

Si vous avez une base de code existante avec des centaines de tests XCTest, pas de panique. La migration peut — et devrait — être progressive. J'ai personnellement migré un projet avec plus de 400 tests XCTest, et la clé c'est vraiment d'y aller par étapes.

Coexistence des deux frameworks

Swift Testing et XCTest cohabitent parfaitement dans le même target de test. Pas besoin de tout migrer d'un coup :

import XCTest
import Testing

// Ce test XCTest fonctionne toujours
class AncienTest: XCTestCase {
    func testAncienneLogique() {
        XCTAssertEqual(2 + 2, 4)
    }
}

// Ce test Swift Testing fonctionne dans le même fichier
@Test("Nouvelle logique")
func nouvelleLogique() {
    #expect(2 + 2 == 4)
}

Règle absolue : ne mélangez jamais les assertions des deux frameworks dans un même test. N'utilisez pas #expect dans un XCTestCase, et n'utilisez pas XCTAssert dans un test @Test. Ça a l'air évident, mais croyez-moi, c'est une erreur facile à faire quand on jongle entre les deux.

Guide de migration pas à pas

Voici l'approche recommandée par Apple et validée par la communauté :

  1. Écrivez tous les nouveaux tests avec Swift Testing.
  2. Migrez les tests existants opportunistiquement : quand vous corrigez un bug ou modifiez la logique d'un test XCTest, profitez-en pour le migrer.
  3. Transformez les classes en structures : remplacez class MonTest: XCTestCase par @Suite struct MonTest.
  4. Remplacez setUp/tearDown par init/deinit.
  5. Convertissez les assertions : XCTAssertEqual(a, b)#expect(a == b), XCTUnwrap(x)try #require(x).
  6. Exploitez les tests paramétrés pour simplifier les tests répétitifs.

Tableau de correspondance rapide

// XCTest                          → Swift Testing
// -------                          → -------------
// class Foo: XCTestCase            → @Suite struct Foo
// func testBar()                   → @Test func bar()
// XCTAssertTrue(x)                 → #expect(x)
// XCTAssertEqual(a, b)             → #expect(a == b)
// XCTAssertNil(x)                  → #expect(x == nil)
// XCTAssertThrowsError(expr)       → #expect(throws: Error.self) { expr }
// XCTUnwrap(optional)              → try #require(optional)
// XCTestExpectation + wait          → confirmation { ... }
// setUp() / tearDown()              → init / deinit
// continueAfterFailure = false      → #require au lieu de #expect

Gardez ce tableau sous la main — il vous fera gagner un temps fou pendant la migration.

Attention au parallélisme

C'est le piège numéro un lors de la migration, et je ne plaisante pas. XCTest exécutait les tests séquentiellement par défaut (ou en parallèle via des processus séparés). Swift Testing, lui, exécute tout en parallèle dans le même processus grâce à Swift Concurrency.

Si vos tests XCTest partagent un état mutable (base de données singleton, UserDefaults, fichiers temporaires), vous aurez des échecs intermittents après migration. Les solutions :

  • Ajoutez .serialized aux suites qui accèdent à des ressources partagées
  • Mieux encore : isolez l'état — chaque test crée sa propre instance de base de données, son propre répertoire temporaire, etc.
  • Utilisez des acteurs pour protéger l'accès concurrent à l'état partagé

Ce qui ne peut pas (encore) être migré

Swift Testing ne couvre pas encore deux domaines importants :

  • Tests d'interface utilisateur : les tests XCUITest ne peuvent pas être migrés vers Swift Testing. Continuez à utiliser XCTest pour ces tests.
  • Tests de performance : les mesures XCTMetric et measure { } n'ont pas d'équivalent dans Swift Testing.

Ce n'est pas dramatique — ces cas restent bien servis par XCTest, et les deux frameworks coexistent sans problème.

Bonnes pratiques pour des tests durables

Préférez les structures aux classes

Apple recommande d'utiliser des structures pour vos suites de tests. Elles permettent au compilateur d'appliquer les vérifications de sécurité de la concurrence, et garantissent que chaque test reçoit une copie indépendante de l'état initial.

Nommez vos tests de manière descriptive

La chaîne passée à @Test est votre meilleur allié pour la lisibilité. Décrivez le comportement attendu, pas l'implémentation :

// ✅ Bon : décrit le comportement
@Test("Un panier vide a un total de zéro")

// ❌ Mauvais : décrit l'implémentation
@Test("testCalculerTotal retourne 0")

Votre futur vous (et vos collègues) vous remercieront quand il faudra comprendre un test qui échoue à 17h un vendredi.

Utilisez les tags pour les catégories transversales

Définissez un ensemble de tags cohérents pour votre projet et appliquez-les systématiquement :

extension Tag {
    @Tag static var unitaire: Self
    @Tag static var intégration: Self
    @Tag static var critique: Self
    @Tag static var lent: Self
}

Un test = un comportement

Chaque fonction de test devrait vérifier un seul comportement. Si vous vous retrouvez avec plus de cinq #expect dans un même test, c'est probablement le signe qu'il faut le découper.

Exploitez init pour le setup commun

@Suite("Tests du convertisseur de devises")
struct ConvertisseurTests {
    let convertisseur: ConvertisseurDevises

    init() async throws {
        convertisseur = try await ConvertisseurDevises.avecTauxActuels()
    }

    @Test("EUR vers USD")
    func eurVersUsd() {
        let résultat = convertisseur.convertir(100, de: .eur, vers: .usd)
        #expect(résultat > 0)
    }

    @Test("USD vers JPY")
    func usdVersJpy() {
        let résultat = convertisseur.convertir(100, de: .usd, vers: .jpy)
        #expect(résultat > 0)
    }
}

Chaque test reçoit sa propre instance du ConvertisseurDevises, initialisée de manière asynchrone si nécessaire. C'est beaucoup plus propre que le setUp()/tearDown() de XCTest — et surtout, c'est du Swift idiomatique.

Intégration avec Swift Concurrency

Swift Testing a été conçu dès le départ pour fonctionner main dans la main avec Swift Concurrency. Toutes les fonctions de test peuvent être async et/ou throws, sans cérémonie :

@Test("Récupère les données météo avec succès")
func donnéesMétéo() async throws {
    let service = ServiceMétéo()
    let météo = try await service.obtenirMétéo(ville: "Paris")

    #expect(météo.température != nil)
    #expect(météo.ville == "Paris")
}

Comme les tests s'exécutent en parallèle par défaut, ils tirent pleinement parti du modèle de concurrence structurée de Swift. Le résultat : des suites de tests significativement plus rapides, surtout si vous avez de nombreux tests qui effectuent des opérations réseau ou d'entrée/sortie.

Si certains de vos tests interagissent avec des API liées au MainActor (comme UIKit ou SwiftUI), vous pouvez les annoter :

@Test("La vue se met à jour après le chargement")
@MainActor
func miseÀJourVue() async throws {
    let viewModel = ArticleViewModel()
    await viewModel.charger()
    #expect(viewModel.articles.count > 0)
}

Et avec l'arrivée d'Approachable Concurrency dans Swift 6.2, qui permet d'opter pour une isolation MainActor par défaut au niveau du module, la gestion de l'isolation dans les tests est devenue encore plus simple.

Conclusion : le futur des tests en Swift

Swift Testing représente un bond en avant considérable pour la qualité logicielle dans l'écosystème Apple. Pour résumer ses avantages clés :

  • Syntaxe expressive : @Test, #expect et #require remplacent des dizaines d'API verbeuses de XCTest
  • Tests paramétrés : réduisent drastiquement le code dupliqué
  • Organisation flexible : suites, tags, traits et hiérarchies imbriquées
  • Concurrence native : exécution parallèle par défaut, intégration transparente avec async/await
  • Swift 6.2 : exit tests, attachments et confirmations étendues comblent les dernières lacunes

XCTest n'est pas obsolète — il reste indispensable pour les tests d'interface utilisateur et les tests de performance. Mais pour les tests unitaires et d'intégration, Swift Testing est clairement l'avenir.

La stratégie la plus sage ? Adoptez Swift Testing pour tout nouveau code et migrez progressivement les tests existants. Pas besoin de tout réécrire d'un coup — la coexistence entre les deux frameworks est vraiment bien gérée.

Si vous avez suivi nos précédents articles sur Approachable Concurrency et Span dans Swift 6.2, vous avez maintenant une vue d'ensemble complète de l'écosystème Swift moderne : un langage plus accessible pour la concurrence, plus performant pour la gestion mémoire, et désormais doté d'un framework de tests à la hauteur de ses ambitions.

Alors, prêt à écrire votre premier @Test ?

À propos de l'auteur Editorial Team

Our team of expert writers and editors.