Bei der Migration von XCTest-Unit-Tests auf einem Mac sollte die Reihenfolge der Besitzfreigabe erhalten bleiben: Arbeit abschließen, die vorgesehenen starken Referenzen freigeben und anschließend die schwache Referenz prüfen. Für die synchrone Bereinigung macht ein lokales defer im Test diese Reihenfolge deutlich.
Dieser Leitfaden behandelt die Umstellung eines Unit-Tests mit besonderem Augenmerk auf Besitzverhältnisse in Xcode. Das Beispiel berechnet die Anzahl der Zellen in einem künstlich erzeugten Raster und prüft anschließend, ob der Rechner freigegeben wird. Es verwendet ausschließlich lokale Swift-Objekte und ganzzahlige Arithmetik.
Welches Lifecycle-Verhalten muss die Migration beibehalten?
XCTest stellt eine vom Framework verwaltete Abfolge bereit. Nach dem Ende einer Testmethode führt es registrierte Teardown-Blöcke in umgekehrter Registrierungsreihenfolge aus, gefolgt von den Teardown-Methoden der Instanz. Registrierte Bereinigung wird auch ausgeführt, wenn eine Test-Assertion fehlschlägt; eine Prozessbeendigung kann dies verhindern. Diese Details sind wichtig, wenn ein Block ein Objekt freigibt und ein anderer dessen schwache Referenz prüft. Zustand in Tests einrichten und aufräumen
Ein Teardown-Block, der beispielsweise auf nil prüft, muss nach dem Block ausgeführt werden, der den letzten vorgesehenen Besitzer entfernt. Da XCTest die Blöcke in umgekehrter Registrierungsreihenfolge ausführt, wird die Prüfung zuerst und die Freigabe danach registriert. Eine Umkehrung dieser Registrierungen ändert, was der Test beobachtet.
Der Migrationsleitfaden für Swift Testing ordnet Setup einem Initialisierer und Teardown einem Deinitialisierer in einer Klassen- oder Actor-Suite zu. Diese Zuordnung eignet sich für Bereinigung, die an die Lebensdauer der Suite gebunden ist. Bei einer Assertion zu Besitzverhältnissen ist eine weitere Entscheidung nötig: Welche Referenz soll genau verschwinden, bevor die Assertion ausgeführt wird? Einen Test von XCTest migrieren
Verwende diese Entscheidungstabelle, bevor du die Syntax des Tests änderst:
| Bisherige Aufgabe | Migrationsoption | Begründung |
|---|---|---|
| Für jeden Instanztest neuen Fixture-Zustand erzeugen | Initialisierung einer gespeicherten Eigenschaft oder init() |
Erstellt das Fixture vor der Ausführung des Tests |
| Ein Objekt freigeben und sofort eine schwache Referenz prüfen | Explizite Freigabe gefolgt von #expect, üblicherweise in einem lokalen defer |
Macht den Beobachtungszeitpunkt sichtbar |
| Synchrone Bereinigung ausführen, die an die Lebensdauer einer Suite mit Referenztyp gebunden ist | Klassen- oder Actor-Suite mit deinit |
Folgt der dokumentierten Lifecycle-Zuordnung |
| Auf Bereinigung warten oder einen Bereinigungsfehler melden | Abgewartete Bereinigung in einer asynchronen Funktion oder ein geeigneter benutzerdefinierter Scope | Erfordert einen Ausführungspfad, der Unterbrechungen und Fehlerbehandlung unterstützt |
Die Tabelle dient als Entscheidungshilfe für die Migration und leitet sich aus dem dokumentierten Lifecycle und den Sprachregeln ab. Sie bedeutet nicht, dass es für jeden XCTest-Hook eine austauschbare Methode in Swift Testing gibt.
Warum kann es den Test verändern, die Prüfung der schwachen Referenz nach deinit zu verschieben?
Swift ruft den Deinitialisierer einer Klasse unmittelbar vor der Freigabe der Instanz auf. Im Deinitialisierer kann noch auf die gespeicherten Eigenschaften dieser Instanz zugegriffen werden. Daher beweist der Eintritt in deinit einer Suite für sich genommen nicht, dass ein Objekt, auf das eine dieser Eigenschaften verweist, bereits freigegeben wurde. Deinitialisierung
Betrachte eine Suite mit einer starken Eigenschaft layout. Eine schwache Referenz auf dieses Layout kann noch nicht-nil sein, während der Deinitialisierer der Suite ausgeführt wird. Der Test muss die starke Eigenschaftsreferenz entfernen, bevor er prüft, ob das Layout freigegeben wurde.
Es gibt noch einen weiteren möglichen Fehlerfall: Ein Referenzzyklus, der die Suite festhält, kann verhindern, dass ihr Deinitialisierer ausgeführt wird. Eine Assertion, die nur in diesem Deinitialisierer steht, kann die Retention dann nicht melden. Das folgt aus den Besitzregeln von ARC: Starke Referenzen halten Instanzen am Leben, während schwache Referenzen die Freigabe nicht verhindern und zu nil werden, wenn die referenzierte Instanz freigegeben wird. Automatische Referenzzählung
Bei einem Test zu Besitzverhältnissen sollte der Beobachter außerhalb des Objekts liegen, dessen Freigabe geprüft wird. Gib dem Test eine konkrete Freigabeoperation und führe seine Assertion über den normalen Kontrollfluss des Tests aus. So lässt sich der Vertrag des Tests nachvollziehen, ohne von der Freigabe der Suite selbst abhängig zu sein.
Die XCTest-Ausgangsbasis in Xcode erstellen
Verwende ein bestehendes Unit-Test-Target in Xcode 16 oder neuer. Apple dokumentiert, dass XCTest und Swift Testing im selben Target und Bundle nebeneinander eingesetzt werden können; für die Migration ist also kein weiteres Unit-Test-Bundle erforderlich. Um eine Testdatei hinzuzufügen, wähle File > New > New File From Template und dann je nach Bedarf XCTest Unit Test oder Swift Testing Unit Test. Tests zum Xcode-Projekt hinzufügen
Das Folgende ist eine vollständige XCTest-Testdatei. Ihr Rechner gibt 3 * 4 zurück, also ist das erwartete Ergebnis 12. Die beiden Teardown-Registrierungen machen die erforderliche Ausführungsreihenfolge ausdrücklich sichtbar.
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
)
}
}
Der Freigabeblock hält den Testfall fest, damit er die Fixture-Eigenschaft aktualisieren kann. Der Beobachtungsblock erfasst den Rechner schwach und vermeidet so einen zusätzlichen starken Besitzer in der Prüfung selbst. Apple dokumentiert Teardown-Blöcke als Ort für testspezifische Bereinigung, die zusammen mit dem zugehörigen Setup registriert wird. Zustand in Tests einrichten und aufräumen
Der Vertrag der Ausgangsbasis ist eng gefasst: Nachdem der Testkörper beendet und die Fixture-Eigenschaft geleert wurde, darf der Rechner nicht mehr existieren. Der Test misst weder den Speicherverbrauch noch belegt er, dass jedes von der Anwendung erzeugte Objekt freigegeben wurde.
Die Reihenfolge der Besitzfreigabe in einen lokalen Scope verschieben
Verwende für diesen synchronen Fall eine Struct-Suite und füge Freigabe und Prüfung in denselben defer-Block ein. Swift führt zurückgestellten Code aus, wenn die Ausführung den umschließenden Scope verlässt, auch bei einer vorzeitigen Rückgabe oder einem geworfenen Fehler. Mehrere zurückgestellte Blöcke werden in umgekehrter Reihenfolge ausgeführt; mit einem einzelnen Block ist die Reihenfolge Freigabe vor Prüfung in diesem Beispiel leichter zu erkennen. Kontrollfluss
Das Folgende ist eine vollständige, eigenständige Swift-Testing-Datei. Ersetze damit die Ausgangsdatei oder behalte die Beispiele in getrennten Dateien, sodass ihre privaten Hilfstypen getrennt bleiben. Der zweite Test ist eine diagnostische Kontrolle: Er zeigt, dass ein zusätzlicher starker Besitzer die schwache Referenz belegt hält, bis dieser Besitzer freigegeben wird.
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)
}
}
Im ersten Test besitzt das Fixture den Rechner, und observed beobachtet ihn, ohne ihn zu besitzen. Die Berechnung ergibt einen optionalen Integer, den #require entpackt. Ein entpackter Rechner wird nicht in einer anderen lokalen Variable gespeichert. Beim Verlassen des Scopes leert der zurückgestellte Block die Referenz des Fixtures und prüft den Beobachter.
Ein fehlgeschlagenes #expect zeichnet ein Problem auf und setzt die Ausführung fort. Ein fehlgeschlagenes #require wirft einen Fehler und beendet diesen Ausführungspfad. Da der defer vor der Anforderung registriert wird, werden Bereinigung und Prüfung der schwachen Referenz weiterhin ausgeführt, wenn die Anforderung einen Fehler wirft. Erwartungen und Bestätigungen, Fehlerbehandlung
Die diagnostische Kontrolle bestätigt zwei getrennte erwartete Zustände: Wird einer von zwei Besitzern freigegeben, bleibt der Beobachter nicht-nil; wird der verbleibende Besitzer freigegeben, ist er nil. Das sind beispielhafte Prüfungen der Besitzverhältnisse und keine gemessenen Anwendungsergebnisse. Die Swift-Beispiele wurden geprüft, konnten in der Rechercheumgebung jedoch nicht kompiliert werden, weil dort keine Swift-Executable verfügbar war.
Grenzen von Hilfsfunktionen, Nebenläufigkeit und asynchroner Bereinigung prüfen
Ein lokales defer gehört zu seinem umschließenden Scope. Wird der zurückgestellte Block in eine Factory-Hilfsfunktion verschoben, wird er ausgeführt, wenn diese Funktion zurückkehrt. Das kann geschehen, bevor der Test das zurückgegebene Objekt verwendet. Lass den Block im Test oder entwirf eine Hilfsfunktion, die die gesamte Operation innerhalb ihres Scopes ausführt. Der umschließende Scope gehört zum Vertrag der Besitzverhältnisse. Kontrollfluss
Prüfe auch, wo sich das Fixture befindet. Swift Testing erstellt für jede Instanztestmethode eine eigene Suite-Instanz; Tests innerhalb einer Suite werden standardmäßig parallel ausgeführt. Instanzlokaler oder testlokaler Zustand passt zu diesem Modell. Ein globales Fixture oder eine statische Sammlung führt dagegen zu gemeinsamem Besitz über mehrere Tests hinweg. Testfunktionen mit Suite-Typen organisieren
Wenn Mac-Anwendungscode den Main Actor erfordert, isoliere den Swift-Testing-Test ausdrücklich mit @MainActor. Der Migrationsleitfaden weist darauf hin, dass Swift Testing das standardmäßige Main-Actor-Verhalten von XCTest für synchrone Testmethoden nicht übernimmt. Der künstliche Rechner oben hat diese Anforderung nicht. Einen Test von XCTest migrieren
Der synchrone zurückgestellte Block ist kein Ersatz für abgewartetes Teardown. Swift erlaubt es zurückgestellten Aktionen nicht, einen Fehler aus dem Block herauszuwerfen. Wenn die Bereinigung fehlschlagen kann, behandle und melde diesen Fehler ausdrücklich, statt ihn zu verwerfen. Lass abgewartete Bereinigung in einem asynchronen Ausführungspfad stattfinden und stelle sicher, dass sie sowohl bei erfolgreicher als auch bei fehlerwerfender Ausführung des Testkörpers erreicht wird. Fehlerbehandlung
Für wiederverwendbares, vom Framework verwaltetes Setup und Teardown bietet Swift Testing derzeit TestScoping, das benutzerdefinierte Ausführung rund um einen Test oder eine Suite ermöglicht. Apple führt es mit Verfügbarkeit ab Swift 6.1 und Xcode 16.3 auf. Es ist ein unterstützter Ansatz, wenn mehrere Tests einen gemeinsamen Scope benötigen, setzt aber ein benutzerdefiniertes Trait und einen Scope-Provider voraus und besteht nicht einfach darin, addTeardownBlock umzubenennen. TestScoping
Eine schwache Referenz prüfen, deren Zustand sich nach der Migration ändert
Wenn der migrierte Test zu Besitzverhältnissen fehlschlägt, prüfe die folgenden Punkte der Reihe nach:
- Beobachtungszeitpunkt bestätigen. Die vorgesehene Freigabe muss vor der Prüfung der schwachen Referenz stattfinden.
- Fixture-Eigenschaften prüfen. Eine verbleibende Eigenschaft kann den Rechner weiterhin besitzen, nachdem eine andere Eigenschaft geleert wurde.
- Lokale Aliase und Closure-Erfassungen prüfen. Ein entpacktes Objekt oder eine stark erfasste Referenz kann den Besitz bis zur Prüfung verlängern.
- Hilfsfunktions-Scope prüfen. Die zurückgestellte Bereinigung einer Hilfsfunktion kann zu früh für den beabsichtigten Vertrag des Tests ausgeführt werden.
- Noch laufende Arbeit prüfen. Wenn das echte Objekt asynchrone Arbeit startet, lege deren Abschluss fest und warte darauf, bevor du eine synchrone Freigabe erwartest.
Eine nicht-nil schwache Referenz zeigt, dass das Objekt zum Beobachtungszeitpunkt noch existiert. Untersuche den verbleibenden Besitzer, bevor du die Assertion änderst oder eine Verzögerung hinzufügst. In diesem künstlichen Beispiel gibt es keine asynchrone Arbeit, auf die gewartet werden müsste, und die diagnostische Kontrolle zeigt genau, wie sich ein zusätzlicher Besitzer auf das Ergebnis auswirkt.
Die Migration ist abgeschlossen, wenn Berechnung und Assertions zu Besitzverhältnissen dasselbe beabsichtigte Ergebnis wie die Ausgangsbasis ausdrücken: 12 berechnen, den vorgesehenen Besitz des Fixtures entfernen und die Freigabe über eine unabhängig gehaltene schwache Referenz prüfen. Halte diese Reihenfolge auch bei späteren Änderungen des Tests sichtbar.
Quellen
Zustand in Tests einrichten und aufräumen — Apple Developer Documentation.
Einen Test von XCTest migrieren — Swift-Projekt.
Deinitialisierung — Swift-Projekt, Die Swift-Programmiersprache.
Automatische Referenzzählung — Swift-Projekt, Die Swift-Programmiersprache.
Tests zum Xcode-Projekt hinzufügen — Apple Developer Documentation.
Kontrollfluss — Swift-Projekt, Die Swift-Programmiersprache.
Erwartungen und Bestätigungen — Swift-Projekt.
Fehlerbehandlung — Swift-Projekt, Die Swift-Programmiersprache.
Testfunktionen mit Suite-Typen organisieren — Swift-Projekt.
TestScoping — Apple Developer Documentation.