Damit XCTest-Fälle einzeln und als Suite bestehen, sollte jeder Test ein eigenes veränderbares Fixture erhalten, das in den zu testenden Code injiziert wird. Wenn ein Singleton gemeinsam genutzt werden muss, setzen Sie es vor jedem Test zurück und verhindern Sie während der gesamten Testlaufzeit überlappende Zugriffe.

Dieser Leitfaden behandelt eine konkrete Entwicklungsaufgabe auf dem Mac: Layoutberechnungstests in Xcode zu reparieren, die einzeln bestehen, zusammen jedoch fehlschlagen. Das Beispiel verwendet ausschließlich synthetische ganzzahlige Breiten und lokale Berechnungen. Es benötigt kein App-Fenster, keinen Service-Mock, keine Dateispeicherung und keine Netzwerkverbindung.

Warum kann ein Test einzeln bestehen und in der Suite fehlschlagen?

Ein isoliert ausgeführter Test trifft möglicherweise auf den Anfangszustand eines Singletons. In einer Suite kann ein früherer Test diesen Zustand bereits verändert haben. Eine neue Referenz auf dieses Singleton verweist weiterhin auf dasselbe veränderbare Objekt.

Swift unterscheidet zwischen Instanz- und Typ-Eigenschaften: Jede Instanz besitzt eigene Instanzeigenschaften, während eine Typ-Eigenschaft nur einmal gemeinsam vorhanden ist. Gespeicherte Typ-Eigenschaften werden beim ersten Zugriff einmalig initialisiert. Wenn Sie SpacingCalculator.shared einer Test-Eigenschaft zuweisen, entsteht daher kein neuer Calculator. Siehe die Dokumentation zu Eigenschaften des Swift-Projekts.

Betrachten Sie eine synthetische Zeile mit Elementen der Breite [10, 20, 30]. Ihre Gesamtbreite ist die Summe der Elementbreiten zuzüglich der Abstände zwischen ihnen. Bei einem Standardabstand von 2 beträgt das Ergebnis 64; bei einem Abstand von 8 beträgt es 76.

Angenommen, ein Test prüft die Standardbreite, ohne den Calculator zu konfigurieren, während ein anderer den gemeinsamen Abstand auf 8 ändert. Die folgende Tabelle beschreibt eine sequenzielle Ausführung, bei der jede Zeile mit einem neuen Calculator beginnt, dessen Abstand 2 beträgt. Dies sind beispielhafte Berechnungen, keine aufgezeichneten Xcode-Testergebnisse.

Ausführung Gemeinsamer Calculator ohne Zurücksetzen Neuer Calculator pro Test
Standardtest allein 64: besteht 64: besteht
Test mit großem Abstand allein 76: besteht 76: besteht
Standard, dann großer Abstand Beide bestehen Beide bestehen
Großer Abstand, dann Standard Standard erhält 76: schlägt fehl Beide bestehen

Das liefert eine hilfreiche Diagnose: Prüfen Sie, welcher Test den Zustand ändert, den die fehlschlagende Assertion verwendet. Testnamen so zu ändern, dass eine günstige Reihenfolge entsteht, beseitigt die Abhängigkeit nicht.

Welche XCTest-Lifecycle-Methode sollte das Fixture verwalten?

Verwenden Sie eine Instanz-Setup-Methode für veränderbaren Zustand, den jeder Test benötigt. Apple dokumentiert, dass das Instanz-Setup vor jeder Testmethode ausgeführt wird, während das Setup auf Klassenebene einmal vor der Testklasse läuft. Ein im Klassen-Setup erzeugtes veränderbares Fixture erhält daher zwischen den Methoden keine neue Ausgangsbasis. Siehe Zustand in Tests einrichten und aufräumen.

Wählen Sie die Setup-Methode passend zur Arbeit: synchrone, nicht werfende Fixture-Erstellung passt zu setUp(); werfende Erstellung zu setUpWithError(); asynchrone Vorbereitung zu setUp() async throws. Verwenden Sie Teardown, um die Bereinigung des aktuellen Tests abzuschließen. Apple rät ausdrücklich davon ab, Teardown zur Vorbereitung nachfolgender Tests zu verwenden, da ein Absturz dessen Ausführung verhindern kann. Dieselbe Lifecycle-Dokumentation beschreibt diese Grenzen.

Die Entscheidung für ein Fixture hängt sowohl von der Besitzverhältnissen als auch vom Ausführungsumfang ab:

Situation Geeigneter Ansatz Zu prüfende Bedingung
Tests können die Abhängigkeit selbst erstellen Pro Test eine Instanz erstellen und injizieren Der Verbraucher muss diese injizierte Instanz verwenden
Tests müssen das tatsächliche Singleton prüfen Vor jedem Test eine vollständige Ausgangsbasis herstellen Kein überlappender Test darf dieses Singleton verändern
Gemeinsame Konfiguration ist unveränderlich Den unveränderlichen Wert wiederverwenden Auch referenzierte Objekte müssen unverändert bleiben
Ein Test startet asynchrone Arbeit Vor Testende auf deren Abschluss warten Danach darf keine unfertige Arbeit das Fixture verändern

Die erste Option ist für diesen Calculator am einfachsten. Der Produktionscode kann einen gemeinsam genutzten Standardwert beibehalten, während Tests ausdrücklich unabhängige Instanzen auswählen.

Ein unabhängig besessenes XCTest-Fixture erstellen

Verwenden Sie einen Mac mit Xcode und einem vorhandenen macOS-Unit-Test-Target. Fügen Sie das folgende vollständige Beispiel als eine Swift-Datei zu diesem Test-Target hinzu. Es definiert einen eigenen Calculator, Verbraucher und Tests und benötigt daher keinen Import eines App-Moduls. Verwenden Sie eine Swift-6-Toolchain, bei der die Standard-Actor-Isolation des Targets auf nonisolated gesetzt ist.

Der Calculator ist ein Actor, damit die Nebenläufigkeitsgrenze seines veränderbaren Zustands ausdrücklich festgelegt ist. Swift-Actors beschränken den Zugriff auf ihren veränderbaren Zustand; Aufrufe von außerhalb des Actors verwenden await. Diese Regeln sind im Concurrency-Kapitel des Swift-Projekts dokumentiert. Auch mit einem Actor bleibt der unabhängige Besitz des Fixtures notwendig.

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)
    }
}

Entscheidend ist die Zeile calculator = SpacingCalculator(). Jedes Setup erstellt einen unabhängigen Actor mit dem Abstand 2. Jeder Test übergibt diesen Actor anschließend an RowLayout. Ein Aufruf von RowLayout() innerhalb dieser Tests würde .shared auswählen und das Fixture umgehen.

Das Beispiel erlaubt absichtlich die direkte Instanziierung. Wenn Ihr vorhandenes Singleton einen privaten Initializer hat, führen Sie einen internen Konstruktionspfad ein oder lagern Sie den veränderbaren Berechnungszustand in eine separat instanziierbare Abhängigkeit aus. Ein interner Initializer kann dem Test-Target über die etablierte Testable-Import-Konfiguration des App-Moduls zugänglich gemacht werden; die Singleton-Referenz global austauschbar zu machen, ist nicht erforderlich.

Die Berechnung definiert außerdem zwei hilfreiche Randfälle: Eine leere Zeile hat die Breite 0, und ein einzelnes Element benötigt keinen Abstand dazwischen. Das Beispiel akzeptiert nicht negative Ganzzahlen, die klein genug sind, um einen Integer-Überlauf zu vermeiden; negative Werte verletzen die Vorbedingungen und beenden die Ausführung, statt einen Assertion-Fehler zu erzeugen.

Die erwartete Berechnung und beide Reihenfolgen der zwei Tests wurden lokal mit deterministischen Python-Berechnungen überprüft. Die Swift-Beispiele wurden geprüft, aber nicht kompiliert oder ausgeführt, da dieser Rechercheumgebung keine Swift-Toolchain zur Verfügung steht. Führen Sie sie in Xcode aus, um Kompilierung und Ausführung für Ihr Target zu verifizieren.

Wann reicht es aus, das Singleton zurückzusetzen?

Das Zurücksetzen ist ein praktikabler Übergangsansatz, wenn Tests tatsächlich die gemeinsame Instanz benötigen und sich ihr gesamter für Tests sichtbarer Zustand wiederherstellen lässt. Stellen Sie vor jedem Test die Ausgangsbasis her, einschließlich der Werte, die ein Test als Standardwerte voraussetzt. Nur die Eigenschaft zurückzusetzen, die der aktuell fehlschlagende Test verändert, kann weitere unentdeckte Abhängigkeiten bestehen lassen.

Für diesen Calculator könnte ein asynchrones Setup SpacingCalculator.shared auswählen und vor jeder Methode setGap(2) abwarten. Damit wird die Berechnungsgrundlage nur dann hergestellt, wenn kein überlappender Aufrufer den Abstand vor der Assertion ändert. Wenn das Fixture später zwischengespeicherte Ergebnisse oder weitere Konfigurationen erhält, muss die Baseline-Operation auch diese Werte abdecken.

Ein Actor verschafft einem Test über mehrere abgewartete Aufrufe hinweg kein exklusives Eigentum. Test A kann beispielsweise den gemeinsamen Abstand auf 2 setzen, Test B kann ihn auf 8 setzen, und Test A kann anschließend eine Breite mit 8 berechnen. Jeder Actor-Aufruf kann korrekt ausgeführt werden, während die Annahme des Gesamttests fehlschlägt. Das folgt aus den dokumentierten Ausführungs- und Unterbrechungsregeln für Actors in Swift.

Bevor Sie sich für Isolation durch Zurücksetzen entscheiden, ermitteln Sie alle Tests und Hintergrundaufgaben, die auf das gemeinsame Objekt zugreifen können. Wenn Sie die exklusive Nutzung während Vorbereitung, Ausführung und Bereinigung nicht sicherstellen können, injizieren Sie stattdessen einen unabhängigen Zustand. Ein nachträgliches Wiederherstellen einer Momentaufnahme kann nicht verhindern, dass ein anderer Aufrufer während des Tests vorübergehende Änderungen beobachtet.

Wie lässt sich das Fixture auf Swift Testing übertragen?

Swift Testing initialisiert für jede Instanz-Testfunktion eine eigene Suite-Instanz. Eine Instanzeigenschaft, die einen Calculator erstellt, bietet daher dasselbe Besitzmuster ohne XCTest-Setup-Overrides. Eine Eigenschaft, die aus .shared initialisiert wird, verweist weiterhin auf das Singleton. Apple dokumentiert den Instanz-Lifecycle unter Testfunktionen mit Suite-Typen organisieren.

Wenn die zuvor gezeigten Definitionen von SpacingCalculator und RowLayout einmal im selben Test-Target vorhanden sind, bildet dies eine vollständige zusätzliche Swift-Testing-Suite. Erforderlich ist eine Toolchain mit dem Modul Testing, etwa Xcode 16 oder neuer. Ihre Standard-Actor-Isolation sollte ebenfalls auf nonisolated gesetzt sein.

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)
    }
}

Für umfangreichere Vorbereitungen unterstützt Swift Testing Suite-Initialisierer, darunter asynchrone oder werfende Initialisierer. Apples Einen Test von XCTest migrieren erläutert diesen Ersatz für XCTest-Setup-Methoden. Dieses Fixture benötigt keine explizite Bereinigung, da es nur lokalen Berechnungszustand besitzt und keine Hintergrundarbeit startet.

Swift Testing führt Tests standardmäßig parallel aus. Wird .serialized auf eine Suite angewendet, werden ihre enthaltenen Tests und verschachtelten Suites sequenziell ausgeführt. Nicht verhindert wird, dass nicht zugehörige Tests gleichzeitig laufen. Wird das Trait auf eine gewöhnliche, nicht parametrisierte Testfunktion angewendet, hat es keine Wirkung. Diese Einschränkungen sind ausdrücklich unter Tests sequenziell oder parallel ausführen beschrieben.

Daher stellt das Platzieren zweier vom Singleton abhängiger Suites unter getrennten .serialized-Annotationen keine gegenseitige Ausschließung zwischen ihnen her. Ein gemeinsamer serialisierter Parent kann seine Nachkommen koordinieren, sofern alle konkurrierenden Nutzer zu diesem Bereich gehören. Neue Fixtures vermeiden diese Koordinationsanforderung bei diesen Berechnungstests.

Die Ergebnisse einzelner Tests und der Suite in Xcode überprüfen

Verwenden Sie während der gesamten Überprüfung dasselbe Scheme, denselben Testplan und dasselbe Mac-Ziel. Apple beschreibt das Ausführen einzelner Methoden, Suites und des aktiven Testplans unter Tests ausführen und Ergebnisse interpretieren.

  1. Führen Sie jede der vier XCTest-Methoden einzeln über die Wiedergabesteuerung im Editor-Rand aus. Erwartet werden jeweils 64, 76, 0 und 10.
  2. Führen Sie die Klasse RowLayoutTests über die Wiedergabesteuerung neben ihrer Deklaration aus. Alle vier Methoden sollten gemeinsam bestehen.
  3. Wählen Sie Product > Test, um den aktiven Testplan auszuführen. Vergewissern Sie sich, dass diese Tests tatsächlich ausgeführt und nicht übersprungen oder ausgeschlossen wurden.
  4. Wenn Sie das Swift-Testing-Beispiel hinzugefügt haben, führen Sie auch dessen zwei Funktionen einzeln und gemeinsam aus.

Wenn einzelne Tests bestehen, der vollständige Testplan jedoch fehlschlägt, verfolgen Sie das vom fehlschlagenden Verbraucher verwendete Objekt zurück. Achten Sie auf eine fehlende Injektion, einen Helfer, der .shared aufruft, oder asynchrone Arbeit, die nach dem Ursprungstest weiterläuft. Schlägt ein Test allein fehl, prüfen Sie seine Eingaben und die erwartete Berechnung, bevor Sie die Fehlerursache der Reihenfolge in der Suite zuschreiben.

Das fertige Fixture sollte seinen Ausgangszustand im Erstellungspfad des aktuellen Tests sichtbar machen. Bei diesen Layoutberechnungen besitzt jeder Test einen Calculator, übergibt ihn ausdrücklich an den Verbraucher und wartet dessen Arbeit ab. Dadurch ist das erwartete Ergebnis unabhängig davon, welcher benachbarte Test zuerst ausgeführt wurde.

Quellen