Lors de la migration de tests unitaires XCTest sur un Mac, préserver la séquence de propriété : terminer le travail, libérer les références fortes prévues, puis vérifier la référence faible. Pour le nettoyage synchrone, un defer local au test rend cette séquence explicite.

Cette démonstration se concentre sur la conversion d'un test unitaire sensible à la propriété dans Xcode. L'exemple calcule le nombre de cellules dans une grille synthétique, puis vérifie que sa calculatrice est libérée. Il utilise uniquement des objets Swift locaux et de l'arithmétique entière.

Quel comportement de cycle de vie la migration doit-elle préserver ?

XCTest fournit une séquence gérée par le framework. Après la fin d'une méthode de test, il exécute les blocs de nettoyage enregistrés dans l'ordre inverse de l'enregistrement, suivis des méthodes de nettoyage d'instance. Le nettoyage enregistré s'exécute toujours lorsqu'une assertion de test échoue ; la terminaison du processus peut l'empêcher. Ces détails sont importants lorsqu'un bloc libère un objet et qu'un autre vérifie sa référence faible. Configurer et nettoyer l'état dans vos tests

Par exemple, un bloc de nettoyage qui vérifie nil doit s'exécuter après le bloc qui supprime le dernier propriétaire prévu. Comme XCTest utilise l'ordre inverse d'enregistrement, la vérification est enregistrée en premier et la libération en second. Inverser ces enregistrements change ce que le test observe.

Le guide de migration de Swift Testing associe la configuration à un initialiseur et le nettoyage à un désinitialiseur sur une suite de classe ou d'acteur. Cette correspondance est utile pour le nettoyage associé à la durée de vie de la suite. Une assertion de propriété nécessite une décision supplémentaire : précisément quelle référence doit disparaître avant que l'assertion ne s'exécute ? Migrer un test depuis XCTest

Utilisez ce tableau de décision avant de modifier la syntaxe du test :

Responsabilité existante Choix de migration Raison
Construire un état de fixture frais pour chaque test d'instance Initialisation de propriété stockée ou init() Établit la fixture avant l'exécution du test
Libérer un objet et inspecter immédiatement une référence faible Libération explicite suivie de #expect, généralement à l'intérieur d'un defer local Rend le point d'observation visible
Effectuer un nettoyage synchrone lié à la durée de vie d'une suite de type référence Suite de classe ou d'acteur avec deinit Suit la correspondance de cycle de vie documentée
Attendre le nettoyage ou signaler une erreur de nettoyage Nettoyage attendu dans une fonction asynchrone, ou une portée personnalisée appropriée Nécessite un chemin d'exécution prenant en charge la suspension et la gestion d'erreurs

Ce tableau est un aide à la décision de migration dérivé des règles de cycle de vie et de langage documentées. Il n'implique pas que chaque point d'ancrage XCTest ait une méthode Swift Testing interchangeable.

Pourquoi déplacer la vérification faible dans deinit peut-il changer le test ?

Swift appelle le désinitialiseur d'une classe immédiatement avant de désallouer l'instance. Le désinitialiseur peut encore accéder aux propriétés stockées de cette instance. Par conséquent, entrer dans le deinit d'une suite ne prouve pas par lui-même qu'un objet détenu par l'une de ces propriétés a déjà été libéré. Désinitialisation

Considérez une suite avec une propriété layout forte. Une référence faible vers cette mise en page peut rester non-nil pendant l'exécution du désinitialiseur de la suite. Le test doit supprimer la référence forte de la propriété avant de demander si la mise en page a été libérée.

Il existe un autre mode d'échec : un cycle de référence qui retient la suite peut empêcher l'exécution de son désinitialiseur. Une assertion placée uniquement dans ce désinitialiseur n'a alors aucune opportunité de signaler la rétention. Cela découle des règles de propriété d'ARC : les références fortes maintiennent les instances en vie, tandis que les références faibles n'empêchent pas la désallocation et deviennent nil lorsque l'instance référencée est désallouée. Comptage de référence automatique

Pour un test de propriété, gardez l'observateur en dehors de l'objet dont la libération est testée. Donnez au test une opération de libération spécifique et exécutez son assertion via le flux de contrôle normal du test. Cet arrangement rend le contrat du test inspectable sans dépendre de la propre désallocation de la suite.

Établir la ligne de base XCTest dans Xcode

Utilisez une cible de test unitaire existante dans Xcode 16 ou version ultérieure. Apple documente que XCTest et Swift Testing peuvent coexister dans la même cible et le même bundle, donc la migration ne nécessite pas un autre bundle de test unitaire. Pour ajouter un fichier de test, utilisez Fichier > Nouveau > Nouveau fichier depuis le modèle et sélectionnez Test unitaire XCTest ou Test unitaire Swift Testing selon le cas. Ajout de tests à votre projet Xcode

Ce qui suit est un fichier de test XCTest complet. Sa calculatrice retourne 3 * 4, donnant un résultat attendu de 12. Les deux enregistrements de nettoyage exposent délibérément l'ordre d'exécution requis.

import XCTest

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

final class GridCalculatorXCTests: XCTestCase {
    private var calculator: GridCalculator?

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

    func testCellCountAndRelease() {
        weak var observed = calculator

        // Registered first, so this executes second.
        addTeardownBlock { [weak observed] in
            XCTAssertNil(observed)
        }

        // Registered second, so this executes first.
        addTeardownBlock { [self] in
            calculator = nil
        }

        XCTAssertEqual(
            calculator?.cellCount(columns: 3, rows: 4),
            12
        )
    }
}

Le bloc de libération retient le cas de test afin qu'il puisse mettre à jour la propriété de fixture. Le bloc d'observation capture la calculatrice faiblement, évitant un propriétaire fort supplémentaire dans la vérification elle-même. Apple documente les blocs de nettoyage comme l'endroit pour le nettoyage spécifique au test enregistré aux côtés de la configuration associée. Configurer et nettoyer l'état dans vos tests

Le contrat de la ligne de base est défini étroitement : après la fin du corps du test et l'effacement de la propriété de fixture, la calculatrice ne doit plus exister. Il ne mesure pas la consommation mémoire ni n'établit que chaque objet créé par l'application a été libéré.

Migrer la séquence de propriété dans une portée locale

Pour ce cas synchrone, utilisez une suite struct et placez la libération plus la vérification dans un bloc defer. Swift exécute le code différé lorsque le contrôle quitte sa portée englobante, y compris un retour anticipé ou une erreur lancée. Plusieurs blocs différés s'exécutent dans l'ordre inverse ; un seul bloc rend la séquence libération-avant-vérification de cet exemple plus facile à lire. Flux de contrôle

Ce qui suit est un fichier Swift Testing complet et indépendant. Remplacez le fichier de base par celui-ci, ou conservez les exemples dans des fichiers séparés où leurs types d'aide privés restent séparés. Le second test est un contrôle de diagnostic : il démontre qu'un propriétaire fort supplémentaire maintient la référence faible peuplée jusqu'à ce que ce propriétaire soit effacé.

import Testing

private final class GridCalculator {
    func cellCount(columns: Int, rows: Int) -> Int {
        columns * rows
    }
}

private final class GridFixture {
    var calculator: GridCalculator? = GridCalculator()
    var additionalOwner: GridCalculator?
}

struct GridCalculatorTests {
    @Test
    func cellCountAndRelease() throws {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        defer {
            fixture.calculator = nil
            #expect(observed == nil)
        }

        let result = try #require(
            fixture.calculator?.cellCount(columns: 3, rows: 4)
        )
        #expect(result == 12)
    }

    @Test
    func weakProbeDetectsAnAdditionalOwner() {
        let fixture = GridFixture()
        weak var observed = fixture.calculator

        fixture.additionalOwner = fixture.calculator
        fixture.calculator = nil
        #expect(observed != nil)

        fixture.additionalOwner = nil
        #expect(observed == nil)
    }
}

Dans le premier test, la fixture possède la calculatrice, et observed la surveille sans la posséder. Le calcul produit un entier optionnel, que #require déballer. Aucune calculatrice déballée n'est stockée dans une autre variable locale. À la sortie de portée, le bloc différé efface la référence de la fixture et vérifie l'observateur.

Un #expect échoué enregistre un problème et permet à l'exécution de continuer. Un #require échoué lance une erreur, arrêtant ce chemin d'exécution. Parce que le defer est enregistré avant l'exigence, son nettoyage et sa vérification faible s'exécutent toujours lorsque l'exigence lance une erreur. Attentes et confirmations, Gestion des erreurs

Le contrôle de diagnostic établit deux états attendus distincts : effacer l'un des deux propriétaires laisse l'observateur non-nil ; effacer le propriétaire restant le laisse nil. Ce sont des vérifications de propriété illustratifs, pas des résultats d'application mesurés. Les exemples Swift ont été inspectés mais n'ont pas pu être compilés dans l'environnement de recherche car aucun exécutable Swift n'était disponible.

Vérifier les limites des aides, la concurrence et le nettoyage asynchrone

Un defer local appartient à sa portée englobante. Déplacer le bloc différé dans une aide d'usine le fait s'exécuter lorsque cette aide retourne, ce qui peut être avant que le test n'utilise l'objet retourné. Gardez le bloc dans le test, ou concevez une aide qui exécute l'opération entière dans la portée de l'aide. La portée englobante fait partie du contrat de propriété. Flux de contrôle

Vérifiez également où réside la fixture. Swift Testing crée une instance de suite distincte pour chaque méthode de test d'instance, et les tests au sein d'une suite s'exécutent en parallèle par défaut. L'état local à l'instance ou au test correspond à ce modèle ; une fixture globale ou une collection statique introduit une propriété partagée entre les tests. Organisation des fonctions de test avec des types de suite

Pour le code d'application Mac qui nécessite l'acteur principal, isolez explicitement le test Swift Testing avec @MainActor. Le guide de migration note que Swift Testing n'hérite pas du comportement par défaut de l'acteur principal de XCTest pour les méthodes de test synchrones. La calculatrice synthétique ci-dessus n'a pas une telle exigence. Migrer un test depuis XCTest

Le bloc différé synchrone n'est pas un remplacement pour le nettoyage attendu. Les actions différées de Swift ne peuvent pas lancer d'erreur hors du bloc. Si le nettoyage peut échouer, gérez et signalez cet échec explicitement plutôt que de l'ignorer. Maintenez le nettoyage attendu dans un chemin d'exécution asynchrone et assurez-vous que les deux chemins de corps (réussite et erreur) l'atteignent. Gestion des erreurs

Pour la configuration et le nettoyage gérés par le framework réutilisables, Swift Testing actuel fournit TestScoping, qui fournit une exécution personnalisée autour d'un test ou d'une suite. Apple le répertorie avec une disponibilité Swift 6.1 et Xcode 16.3. C'est une direction prise en charge lorsque plusieurs tests ont besoin d'une portée commune, mais cela nécessite un trait personnalisé et un fournisseur de portée plutôt que de simplement renommer addTeardownBlock. TestScoping

Diagnostiquer une vérification faible qui change après la migration

Si le test de propriété migré échoue, inspectez ces points dans l'ordre :

  1. Confirmer le point d'observation. La libération prévue doit se produire avant l'assertion faible.
  2. Inspecter les propriétés de fixture. Une propriété restante peut encore posséder la calculatrice après qu'une autre propriété a été effacée.
  3. Inspecter les alias locaux et les captures de fermeture. Un objet déballé ou une référence fortement capturée peut étendre la propriété au travers de la vérification.
  4. Inspecter la portée de l'aide. Le nettoyage différé d'une aide peut s'exécuter trop tôt pour le contrat prévu du test.
  5. Inspecter le travail en cours. Si l'objet réel démarre un travail asynchrone, définissez et attendez son achèvement avant d'appliquer une attente de libération synchrone.

Une référence faible non-nil identifie un objet qui existe encore au point d'observation. Étudiez le propriétaire restant avant de modifier l'assertion ou d'ajouter un délai. Dans cet exemple synthétique, il n'y a pas de travail asynchrone à attendre, et le contrôle de diagnostic montre exactement comment un propriétaire supplémentaire affecte le résultat.

La migration est terminée lorsque les assertions de calcul et de propriété expriment le même résultat prévu que la ligne de base : calculer 12, supprimer la propriété prévue de la fixture et vérifier la libération via une référence faible détenue indépendamment. Maintenez cette séquence visible au fur et à mesure que le test évolue.

Sources