← Alle Beitrage

Swift Testing: Das Framework, das XCTest ablöst – und was in XCTest bleibt

XCTest, Apples Test-Framework seit 2013, wird durch Swift Testing abgelöst. Apple sagt zwar noch nicht „deprecated” (XCTest wird weiterhin ausgeliefert, funktioniert weiterhin und beherbergt weiterhin UI-Automatisierungs- und Performance-Tests), aber jede Apple-Session, jeder Swift-Forum-Thread und jedes neue Beispielprojekt seit der WWDC 2024 lenkt Swift-Code auf Swift Testing. Das Framework wird mit Xcode 16+ und Swift 6 ausgeliefert1, läuft auf macOS, iOS, watchOS, tvOS, visionOS und Linux2 und präsentiert ein anderes mentales Modell: Tests sind Funktionen, Suites sind Typen, Konfiguration sind Traits und Parallelität ist der Standard.

Die Migration verläuft inkrementell. Apples eigene Dokumentation unterstützt ausdrücklich den parallelen Betrieb beider Frameworks im selben Target3, was bedeutet, dass der richtige Schritt für eine nicht-triviale Codebasis nicht „alles neu schreiben” lautet, sondern „neue Tests in Swift Testing schreiben und die alten migrieren, wenn Sie sie ohnehin anfassen”. Dieser Beitrag behandelt die API-Oberfläche, benennt die Migrationsgrenze (was in XCTest bleibt) und behandelt das Traits-Vokabular, das die klassenhierarchische Konfiguration von XCTest ersetzt.

TL;DR

  • Swift Testing ersetzt XCTests klassenbasiertes, stringly-typed Modell durch Macros: @Test, @Suite, #expect(...) und #require(...)4.
  • Tests sind freie Funktionen oder Methoden auf Typen; Suites sind beliebige Typen, die Testfunktionen enthalten (@Suite ist nur erforderlich, wenn ein Anzeigename oder Trait angegeben werden soll).
  • Konfiguration verschiebt sich von Klassenhierarchien und Namenskonventionen zu Traits: .enabled(if:), .disabled(_:), .serialized, .timeLimit(...), .tags(...), .bug(...)5.
  • Parametrisierte Tests via @Test(arguments:) erzeugen ein übergeordnetes Testergebnis mit einem Kindergebnis pro Argument; standardmäßig parallel6.
  • Die Migration erfolgt parallel, nicht als Big Bang. UI-Automatisierung (XCUIApplication) und Performance-Tests (XCTMetric) bleiben in XCTest, weil Swift Testing diese nicht abdeckt3.
  • Die WWDC 2025 fügte benutzerdefinierte Attachments (Test-Artefakte) und Exit-Tests hinzu (zur Verifikation von Code, der unter bestimmten Bedingungen terminieren soll)7.

Warum Swift Testing existiert

Die Schwachstellen von XCTest sind jedem bekannt, der eine große iOS-Test-Suite betreut hat:

Stringly-typed Assertions. XCTAssertEqual(a, b, "message") erfasst den ursprünglichen Swift-Ausdruck nicht. Wenn die Assertion fehlschlägt, zeigt die Fehlermeldung die Laufzeitwerte, aber nicht, welcher Ausdruck sie erzeugt hat. Debugging erfordert das Lesen des Test-Quellcodes und das Rekonstruieren des Kontexts.

Klassenbasierte Konfiguration. Setup, Teardown und gemeinsamer Zustand erfordern das Subclassing von XCTestCase. Einen Test zu deaktivieren bedeutet entweder, ihn umzubenennen (sodass das Discovery-Pattern ihn nicht erfasst) oder ihn in #if DEBUG zu hüllen. Auswählen, welche Tests laufen, erfordert sorgfältige Benennung.

Concurrency-Mismatch. XCTest stammt aus der Zeit vor Swift Concurrency. XCTestExpectation plus wait(for:timeout:) ist die Legacy-Brücke zu asynchronem Code; das Pattern ist umständlich und fehleranfällig. Moderner Swift-Code verwendet async/await; XCTests Idiome wirken anachronistisch.

Schwache Parametrisierung. XCTest verfügt über keine erstklassige Unterstützung für parametrisierte Tests. Entwickler täuschen sie mit For-Schleifen innerhalb von Testmethoden vor (was ein Testergebnis für viele Fälle erzeugt) oder schreiben Code-Generatoren (die N Testmethoden aus einer Vorlage erzeugen).

Swift Testing geht jeden dieser Punkte an:

import Testing

@Test func userInitializesWithDefaults() {
    let user = User()
    #expect(user.name == "Anonymous")
    #expect(user.preferences.isEmpty)
}

#expect erfasst den vollständigen Ausdruck. Wenn die Assertion fehlschlägt, zeigt die Testausgabe, dass user.name == "Anonymous" fehlschlug, weil user.name den Wert "Guest" hatte. Das Framework erledigt die Arbeit, den Ausdruck zu lesen, weil das Macro den Syntaxbaum sieht. Kein Rätselraten nach dem Schema „erwartet X, erhalten Y”.

Die Macros

Vier Macros erledigen die Arbeit.

@Test und @Suite

@Test markiert eine Funktion als Test. Die Funktion kann auf Dateiebene leben, in einer Struct, in einem Actor, in einem Enum, überall, wo eine Funktion in Swift leben kann. Das Framework entdeckt @Test-markierte Funktionen und führt sie aus.

@Test func loginSucceedsWithValidCredentials() async throws {
    let result = try await api.login(username: "alice", password: "valid")
    #expect(result.isAuthenticated)
}

struct AuthenticationTests {
    @Test func loginRejectsEmptyPassword() async throws {
        let result = try await api.login(username: "alice", password: "")
        #expect(!result.isAuthenticated)
        #expect(result.error == .emptyPassword)
    }
}

@Suite markiert einen Typ explizit als Test-Suite. Der Marker ist implizit, wenn ein Typ @Test-Funktionen enthält; explizites @Suite ist nur erforderlich, wenn ein benutzerdefinierter Anzeigename hinzugefügt oder ein Trait auf Suite-Ebene angewendet wird4.

@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
    @Test func loginSucceeds() async throws { ... }
    @Test func loginFails() async throws { ... }
}

#expect und #require

#expect ist die Standard-Assertion. Der Test wird fortgesetzt, falls die Erwartung fehlschlägt (der Fehler wird aufgezeichnet; nachfolgende Assertions laufen weiterhin). #require ist die Fail-Fast-Variante: Wenn die Anforderung fehlschlägt, stoppt der Test sofort. Verwenden Sie #require für Vorbedingungen, deren Versagen in nachfolgenden Assertions Unsinnsfehler erzeugen würde.

@Test func userPreferencesLoad() async throws {
    let user = try #require(await store.fetchUser(id: 42))
    // If fetchUser returns nil, the test stops here. The next line
    // never runs against an unwrapped optional.
    #expect(user.preferences["theme"] == "dark")
    #expect(user.preferences.count >= 1)
}

Das try vor #require ist entscheidend: #require gibt den entpackten Wert zurück, wenn die Anforderung erfüllt ist (in diesem Fall den nicht-optionalen User), oder wirft, wenn nicht. Die Testfunktion muss throws sein, um #require zum Entpacken zu verwenden.

Parametrisierte Tests

@Test(arguments:) führt die Testfunktion einmal pro Argument aus. Das Framework meldet jedes Argument als Kindertest mit dem übergeordneten Test als Rollup.

@Test(arguments: [
    ("alice", "password123", true),
    ("bob", "", false),
    ("", "password123", false),
    ("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
    let result = try await api.login(username: username, password: password)
    #expect(result.isAuthenticated == expectedSuccess)
}

Das Framework führt die vier Argumente standardmäßig parallel aus. Test-Fehlermeldungen identifizieren, welches Argument-Tupel fehlgeschlagen ist, sodass das Debuggen eines parametrisierten Fehlers nicht erfordert, den Test isoliert auszuführen.

Für Argumente, die unabhängige Collections sind, führt @Test(arguments: arrayA, arrayB) das Kreuzprodukt aus (jede Kombination). Für sequenzielle Paare verwenden Sie zip(arrayA, arrayB) und übergeben die resultierende Sequenz.

Traits: Konfiguration als erstklassige Werte

XCTest verwendet Klassenhierarchien, Namenskonventionen und Xcode-Testpläne, um Tests zu konfigurieren. Swift Testing verwendet Traits: Werte, die auf @Test- und @Suite-Deklarationen angewendet werden5:

@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }

@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }

@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }

@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }

@Suite(.serialized)
struct DatabaseTests {
    // All tests in this suite run sequentially.
    // Useful when tests share a database fixture.
    @Test func insertUser() { ... }
    @Test func updateUser() { ... }
    @Test func deleteUser() { ... }
}

Das Trait-Vokabular deckt die Fälle ab, für die XCTest benutzerdefinierte Infrastruktur benötigte:

  • .enabled(if:) und .disabled(_:). Bedingte Aktivierung mit einem Laufzeitausdruck und einem optionalen Grund. Ersetzt #if DEBUG-Blöcke und Umbenennungstricks.
  • .serialized. Markiert eine Suite oder einen parametrisierten Test als sequenziell. Vererbbar: Eine serialisierte Suite zwingt alle Kindertests zur seriellen Ausführung6.
  • .timeLimit(...). Zeitlimit pro Test. Der Test wird beendet und als fehlgeschlagen gemeldet, wenn er das Limit überschreitet.
  • .tags(...). Tags zum Filtern. Konfigurieren Sie Include-/Exclude-Tag-Regeln in einem Xcode-Testplan; der Plan läuft via xcodebuild test -testPlan <name>. Beachten Sie, dass -only-testing: nach Test-Identifier (Target, Suite oder Funktion) filtert, nicht nach Tag.
  • .bug(...). Verknüpft einen Test mit einer Bug-ID samt Begründung. Die Verknüpfung erscheint im Testbericht und verhindert (für als fehlerhaft bekannte Tests), dass der Fehler die CI blockiert.

Auch benutzerdefinierte Traits werden unterstützt. Eine App kann eigene Traits für projektspezifische Bedürfnisse definieren (z. B. .requiresKeychain, .skipIfOffline).

Parallelität ist der Standard

Swift Testing führt Tests standardmäßig parallel aus, einschließlich parametrisierter Argumente. Der Standard ist wichtig, weil die meisten Test-Suites mit der Zeit wachsen und Parallelität den Unterschied zwischen einem 30-sekündigen Testlauf und einem 5-minütigen ausmacht. XCTest erforderte explizites Opt-in (Test-Parallelität-Toggle im Testplan); Swift Testing kehrt den Standard um.

Die Implikation: Tests müssen unabhängig sein. Gemeinsamer Zustand (eine Datenbank-Fixture, ein gemocktes Dateisystem, ein App-weiter Singleton) muss entweder pro Test isoliert sein (jeder Test erhält eine frische Fixture) oder als .serialized markiert werden (die Suite läuft sequenziell). Tests, die globalen Zustand mutieren und serielle Ausführung voraussetzen, sind Bugs, die Swift Testing schneller aufdeckt als XCTest.

Migration: Parallel, nicht Big Bang

Apples offizielle Position zur Migration ist inkrementell3:

  • Beide Frameworks koexistieren im selben Test-Target.
  • Neue Tests können sofort in Swift Testing geschrieben werden.
  • Alte XCTest-Tests funktionieren ohne Änderungen weiter.
  • Migrieren Sie XCTest-Tests zu Swift Testing, wenn es passt (eine angefasste Datei, ein neues Feature, ein Refactor).

Zwei Fälle bleiben dauerhaft in XCTest:

UI-Automatisierungs-Tests. XCUIApplication, XCUIElement, XCUIElementQuery, die gesamte UI-Test-API, sind XCTest-exklusiv3. Swift Testing bietet keinen Ersatz. UI-Tests bleiben in XCTest, bis Apple die Abdeckung von Swift Testing erweitert.

Performance-Tests. XCTMetric, measure(metrics:), Performance-Baselines, ebenfalls XCTest-exklusiv3. Swift Testing hat dafür noch kein Äquivalent.

Für alles andere (Unit-Tests, Integrationstests, asynchrone Tests, parametrisierte Tests) ist Swift Testing der empfohlene Standard für neuen Code.

Was die WWDC 2025 hinzugefügt hat

Zwei bemerkenswerte Erweiterungen für Swift Testing auf der WWDC 20257:

Benutzerdefinierte Attachments. Ein Test kann beliebige Attachable-Daten an sein Ergebnis anhängen, um die Fehleranalyse zu erleichtern. Fehlgeschlagene Screenshot-Bilder, Diagnose-Logs, generierte Eingabedateien. Die Attachments erscheinen in Xcodes Testreporter und in CI-Artefakten. Die Attachment.record(_:named:sourceLocation:)-API in Sources/Testing/Attachments/Attachment.swift definiert die Oberfläche; Werte, die Attachable entsprechen (Data, String, Bildtypen via Opt-in-Konformität), werden akzeptiert.

@Test func reportLayoutAtFailure() async throws {
    let result = await renderer.run(LayoutTestCase.complex)
    if !result.passes {
        Attachment.record(result.diagnostics, named: "diagnostics.txt")
        Attachment.record(result.screenshotData, named: "failure-screenshot.png")
    }
    #expect(result.passes)
}

Exit-Tests. Tests, die Code verifizieren, der terminieren soll (Aufruf von exit(), Auslösen eines fatalen Fehlers, Auslösen eines Precondition-Failures). XCTest hatte keinen sauberen Weg, diese zu testen; Swift Testings Exit-Tests führen den Code in einem Kindprozess aus und verifizieren das Terminierungsverhalten.

@Test func parserExitsOnMalformedInput() async {
    await #expect(processExitsWith: .failure) {
        Parser.parse(malformedInput)
    }
}

Beide Erweiterungen sind pragmatisch. Benutzerdefinierte Attachments lösen einen echten Schmerzpunkt (Debugging instabiler CI-Fehler); Exit-Tests decken eine echte Lücke in der Unit-Test-Oberfläche ab.

Wann XCTest weiterhin die richtige Wahl ist

Drei Fälle, in denen neuer Code weiterhin XCTest sein sollte:

Der Test ist ein UI-Automatisierungs-Test. XCUIApplication.launch, Tap-und-Swipe-Interaktionen, auf Accessibility-Identifier basierende Queries. Swift Testing deckt die UI-Automatisierungs-Oberfläche nicht ab; neue UI-Tests sind XCTest.

Der Test ist ein Performance-Benchmark. measure(metrics: [XCTClockMetric()]), Performance-Baselines, Speichermessungen. Swift Testing hat keine Äquivalente; neue Performance-Tests sind XCTest.

Der Test muss auf einer Plattform laufen, die Swift Testing nicht unterstützt. Swift Testing erfordert Swift 6 / Xcode 16+. Tests, die gegen ältere Toolchains kompilieren müssen (eine CI-Matrix mit älteren Xcode-Versionen), benötigen XCTest, bis die Toolchain nachgezogen hat.

Für Unit-Tests, Integrationstests, asynchrone Tests und parametrisierte Tests auf Swift-6+-Targets ist Swift Testing der moderne Standard. Die Migrationskosten sind real, doch die Kosten der Nicht-Migration kumulieren sich; jeder heute neu geschriebene XCTest ist technische Schuld.

Die Verbindung zum Agent-Workflow

Tests sind der Vertrag, gegen den AI-gestützte Code-Generierung arbeitet. Wenn ein Agent Swift-Code schreibt, sind die Tests die Art und Weise, wie der Entwickler (oder der Agent selbst) verifiziert, dass die Änderung korrekt ist. Die Eigenschaften von Swift Testing machen agent-generierten Code besser verifizierbar:

  • Ausdruckserfassung. Der Fehlschlag von #expect(user.score == calculator.compute(user)) zeigt beide Seiten des Vergleichs. Ein Agent, der den Fehler liest, kann den Bug beheben, ohne die Testdatei erneut zu lesen.
  • Parametrisierte Fälle. Ein Agent, der eine Regression behebt, weiß, welche spezifische Eingabe fehlgeschlagen ist. Die Korrektur kann gezielt erfolgen.
  • Trait-basierte Filterung. Eine .tags(.regression)-Annotation lässt einen Agenten nach einer Korrektur nur Regressionstests ausführen; schnellere Feedback-Schleife.
  • Benutzerdefinierte Attachments. Ein Agent, der einen instabilen Test debuggt, kann das fehlschlagende Eingabe-Artefakt anhängen und es im nächsten Lauf lesen.

Der Beitrag zu Hooks für die Apple-Entwicklung behandelt Stop-Hooks, die nach jeder Claude Code-Bearbeitung Tests ausführen. Die schnelleren, parallelen, informativeren Fehler von Swift Testing machen diese Schleife eng genug, dass der Agent ohne manuelles Eingreifen iterieren kann.

Was dieses Pattern für iOS-26+-Apps bedeutet

Drei Erkenntnisse.

  1. Setzen Sie neue Tests standardmäßig auf Swift Testing. Das Framework ist ausdrucksstärker, läuft standardmäßig parallel, erfasst Fehlerkontext automatisch und unterstützt parametrisierte Tests als erstklassiges Konzept. Bestehender XCTest-Code funktioniert weiterhin; neuer Code verwendet den modernen Standard.

  2. Belassen Sie UI-Automatisierungs- und Performance-Tests in XCTest. Apples Abdeckungslücke, keine Migrationsentscheidung. Solange Swift Testing nicht auf diese Domänen erweitert wird, bleiben diese Tests dauerhaft in XCTest.

  3. Nutzen Sie Traits als Konfigurationsvokabular. .enabled(if:), .disabled(_:), .serialized, .timeLimit(...), .tags(...), .bug(...) decken die Fälle ab, für die XCTest Klassenhierarchien, Namenskonventionen und #if DEBUG-Blöcke benötigte. Traits sind erstklassige Werte, die sich komponieren lassen; das Trait-Vokabular ist es, was das Framework modern wirken lässt.

Der vollständige Apple-Ecosystem-Cluster: typisierte App Intents; MCP-Server; die Routing-Frage; Foundation Models; die Unterscheidung zwischen Runtime und Tooling LLM; drei Oberflächen; das Single-Source-of-Truth-Pattern; Zwei MCP-Server; Hooks für die Apple-Entwicklung; Live Activities; die watchOS-Runtime; SwiftUI-Internals; RealityKits räumliches mentales Modell; SwiftData-Schema-Disziplin; Liquid-Glass-Patterns; Multi-Platform-Shipping; die Plattform-Matrix; Vision-Framework; Symbol Effects; Core-ML-Inferenz; Writing-Tools-API; worüber ich nicht schreibe. Der Hub befindet sich unter der Apple Ecosystem Series. Für breiteren Kontext zu iOS mit AI-Agenten siehe den iOS Agent Development Guide.

FAQ

Ersetzt Swift Testing XCTest vollständig?

Noch nicht. XCTest beherbergt weiterhin UI-Automatisierungs-Tests (XCUIApplication) und Performance-Tests (XCTMetric); Swift Testing deckt diese Domänen nicht ab. Für Unit-Tests, Integrationstests, asynchrone Tests und parametrisierte Tests ist Swift Testing der empfohlene Standard für neuen Code. Beide Frameworks koexistieren konfliktfrei im selben Target.

Was ist der praktische Unterschied zwischen #expect und XCTAssertEqual?

#expect erfasst den vollständigen Swift-Ausdruck und meldet bei einem Fehlschlag beide Seiten des Vergleichs (z. B. user.score == calculator.compute(user) ist fehlgeschlagen, weil user.score = 80 und calculator.compute(user) = 100). XCTAssertEqual kennt nur die Laufzeitwerte und den optionalen Meldungs-String. Die Ausdruckserfassung ist es, was Swift-Testing-Fehler selbsterklärend macht, ohne den Test-Quellcode erneut zu lesen.

Wie funktionieren parametrisierte Tests?

@Test(arguments: [...]) führt die Testfunktion einmal pro Argument aus. Das Framework meldet jedes Argument als Kindertestergebnis mit dem übergeordneten Test als Rollup. Tupel können mehrere Werte übergeben. Parametrisierte Tests laufen standardmäßig parallel; fügen Sie .serialized hinzu, um sequenzielle Ausführung zu erzwingen.

Wofür ist #require gedacht?

#require ist die Fail-Fast-Variante von #expect. Verwenden Sie es für Vorbedingungen, deren Versagen in nachfolgenden Assertions Unsinnsfehler erzeugen würde. try #require(value) gibt den entpackten Wert zurück, wenn die Bedingung erfüllt ist, oder wirft, wenn nicht, sodass der Rest des Tests den entpackten Wert direkt verwenden kann. Die Testfunktion muss throws sein, um #require zum Entpacken zu verwenden.

Kann ich Swift-Testing-Tests unter Linux schreiben?

Ja. Swift Testing ist Open Source2 und läuft auf jeder Plattform, die Swift unterstützt, einschließlich Linux. Server-seitige Swift-Projekte können Swift Testing sofort übernehmen. Das Framework ist nicht Apple-Plattform-spezifisch.

Wie migriere ich eine bestehende XCTest-Suite?

Apples Empfehlung lautet inkrementell: Schreiben Sie neue Tests in Swift Testing, migrieren Sie alte, wenn Sie sie ohnehin anfassen3. Es gibt keinen automatisierten Konverter (die Modellunterschiede sind groß genug, dass eine Automatisierung brüchig wäre). Das richtige Migrationsziel sind Unit-Tests und Integrationstests; UI-Tests und Performance-Tests bleiben dauerhaft in XCTest.

Referenzen


  1. Apple Developer: Swift Testing. Apples Überblick zur Einführung von Swift Testing mit Xcode 16+ und Swift 6. 

  2. Open-Source-Repository: swiftlang/swift-testing auf GitHub. Der Quellcode des Frameworks, verfügbar unter der Apache License 2.0 mit der Standardlizenz des Swift-Projekts. 

  3. Apple Developer Documentation: Migrating a test from XCTest. Apples offizieller Migrationsleitfaden, der die parallele Koexistenz und die Ausnahmen für XCUIApplication / XCTMetric behandelt. 

  4. Apple Developer Documentation: Swift Testing. Framework-Referenz für @Test, @Suite, #expect, #require und das Trait-Vokabular. 

  5. Apple Developer Documentation: Trait. Das Protokoll, das Test-Traits definiert, mit eingebauten Traits einschließlich .enabled(if:), .disabled(_:), .serialized, .timeLimit(...), .tags(...), .bug(...)

  6. Apple Developer: Meet Swift Testing (WWDC 2024 Session 10179) und Go further with Swift Testing (WWDC 2024 Session 10195). Die Einführungs-Sessions zu parametrisierten Tests und Parallelität. 

  7. Open-Source-Repository-Belege für die WWDC-2025-Erweiterungen: Sources/Testing/Attachments/Attachment.swift für benutzerdefinierte Attachments und Sources/Testing/ExitTests/ für das Exit-Test-Macro #expect(processExitsWith:). Beide tauchten erstmals auf der WWDC 2025 auf und werden mit Xcode 26+ ausgeliefert. 

Verwandte Beiträge

Five Apple Platforms, Three Shared Files: How Return Actually Ships Cross-Platform SwiftUI

Return runs on iPhone, iPad, Mac, Apple Watch, and Apple TV. Three Swift files are shared across all five targets out of…

18 Min. Lesezeit

What SwiftUI Is Made Of

SwiftUI is a result-builder DSL on top of a value-typed View tree. Once the substrate is visible, AnyView, Group, and Vi…

17 Min. Lesezeit

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 Min. Lesezeit