Ein erster Blick auf Swift Testing

Mit dem bald erscheinenden Xcode 16 hält auch ein neues Framework zum Schreiben und Ausführen von Unit-Tests Einzug: Swift Testing. Genau wie zuvor SwiftUI und SwiftData macht Swift Testing exzessiven Gebrauch von Makros und möchte durch Einfachheit und Flexibilität überzeugen.

Die folgende Vorschau zu Swift Testing basiert auf Beta 6 von Xcode 16.

Voraussetzungen und Einschränkungen

Mit Xcode 16 ist es möglich, Unit-Tests mithilfe des neuen Swift Testing-Frameworks zu implementieren. Das grundlegende Vorgehen ist hierbei identisch zum Einsatz von XCTest: Es braucht ein separates Test-Target, in dem alle Unit-Tests umgesetzt werden. Fügt man ein solches ab Xcode 16 hinzu oder erstellt ein neues Projekt, fragt Xcode ab, ob man ausschließlich auf XCTest setzen oder Swift Testing für die Unit-Tests verwenden möchte.

Xcode 16 fragt das gewünschte „Testing System“ ab, das neben XCTest nun auch den Einsatz von Swift Testing erlaubt.
Xcode 16 fragt das gewünschte „Testing System“ ab, das neben XCTest nun auch den Einsatz von Swift Testing erlaubt.

Xcode weist hier auch bereits unmittelbar auf eine aktuelle Einschränkung von Swift Testing hin: Bisher lässt sich Swift Testing ausschließlich zum Schreiben von Unit-Tests verwenden. Für Performance- und UI-Tests kommt weiterhin XCTest zum Einsatz. Erfreulicherweise ist ein Parallelbetrieb beider Frameworks aber kein Problem. Langfristig ist zu erwarten, dass Swift Testing auch Unterstützung für Performance- und UI-Tests erhalten wird.

Schreiben eines ersten Unit-Tests

Um einen Unit-Test mit Swift Testing zu schreiben, braucht es nicht viel. Ist das Framework via import Testing eingebunden, können anschließend beliebige Testmethoden geschrieben werden. Im Gegensatz zu XCTest stellt Swift Testing keinerlei Anforderungen an die Benennung der Methoden; das Präfix test ist entsprechend nicht notwendig. Stattdessen werden Testmethoden mit dem @Test-Makro deklariert, wie das folgende Listing am Beispiel der addition()-Methode zeigt.

import Testing
@testable import TS_Swift_Testing

struct TS_Swift_TestingTests {
    @Test func addition() {
        // TODO: Implement test method.
    }
}

Mit dieser grundlegenden Deklaration steht uns ein Unit-Test zur Verfügung, den wir ausführen können. Doch natürlich ist solch ein Test nichts ohne eine valide Auswertung des Ergebnisses. Als Grundlage für den eben vorbereiteten Test dient eine Structure namens Calculator, die über eine simple Methode zur Addition zweier Zahlen verfügt. Das Ergebnis dieser Addition wird von der Methode zurückgeliefert. Die vollständige Deklaration zeigt das folgende Listing.

struct Calculator {
    func addition(firstValue: Int, secondValue: Int) -> Int {
        firstValue + secondValue
    }
}

Um ein Testergebnis in einem Unit-Test auszuwerten, nutzt man das #expect-Makro. Es erwartet ein Boolean, das den Test-Ausgang definiert; true entspricht einem erfolgreich durchlaufenen Test, false weist auf das Scheitern hin.

Mit diesen Informationen können wir den zuvor vorbereiteten Unit-Test implementieren. Zu diesem Zweck erfolgt innerhalb der addition()-Testmethode die Erstellung einer Calculator-Instanz und der anschließende Aufruf der addition(firstValue:secondValue:)-Methode. Mithilfe des #expect-Makros wird das erwartete Ergebnis auf seine Korrektheit hin überprüft.

@Test func addition() {
    let calculator = Calculator()
    let result = calculator.addition(firstValue: 19, secondValue: 99)
    let expectedResult = 118
    #expect(result == expectedResult)
}

Das Herzstück: #expect

Das neue #expect-Makro übernimmt die Rolle, die in XCTest die verschiedenen Assert-Funktionen einnehmen. Über diesen Aufruf regelt man in Swift Testing den Erfolg oder Misserfolg eines Tests, indem man schlicht ein Boolean zurückliefert. All die Assert-Varianten wie XCTAssertTrue, XCTAssertFalse, XCTAssertNil oder XCTAssertEqual (um nur einige zu nennen) sind damit hinfällig.

Tests mit Error Handling und Concurrency

Tests auf Basis von Swift Testing haben aber noch einen weiteren großen Vorteil: Sie bieten ohne Einschränkungen Unterstützung für Error Handling und Concurrency. Um von diesen Techniken Gebrauch zu machen, deklariert man die Testmethode wie gewohnt mit den entsprechenden Keywords throws und async.

Feuert man innerhalb einer Testmethode einen Fehler, wird das entsprechend protokolliert und der Test als fehlgeschlagen gekennzeichnet. Concurrency erlaubt es, auch nebenläufigen Code in einer Testmethode auszuführen.

Erweitere Konfiguration mit Namen und Suites

Tests in Swift Testing lassen sich in sogenannten Suites gruppieren. Eine solche Suite entsteht automatisch, sobald Unit-Tests als Teil eines Typs deklariert werden. Im gezeigten Beispiel definiert die eingangs deklarierte Structure TS_Swift_TestingTests so eine gleichnamige Suite, unter der alle in diesem Typ deklarierten Tests zusammengefasst werden.

Das @Suite-Makro ermöglicht es, den Namen einer solchen Suite individuell anzupassen, wie das nachfolgende Listing zeigt. Auch die Namen von Tests können individuell definiert werden, indem man einen gewünschten Bezeichner dem @Test-Makro als Parameter übergibt. Die Bezeichner von Suites und Tests tauchen im Test Navigator auf.

@Suite("Calculator Tests") struct TS_Swift_TestingTests {
    @Test("Addition Test") func addition() { ... }
}
Im Test Navigator sind die Namen von Suites und Tests zu sehen.
Im Test Navigator sind die Namen von Suites und Tests zu sehen.

Einsatz von Tags

Auf Wunsch lassen sich Unit-Tests zusätzlich mithilfe von Tags gruppieren. Thematisch zusammenhängende Tests, die nicht Teil derselben Suite sind, lassen sich so ebenfalls zusammenfassen.

Um einen Tag zu erstellen, ist eine Erweiterung des bestehenden Tag-Typs mittels Extension notwendig. In solch einer Extension deklariert man dann einen neuen Tag mithilfe des @Tag-Makros. Im Folgenden werden zwei Tags ergänzt, die sich auf das Erhöhen beziehungsweise Verringern von Zahlenwerten beziehen.

extension Tag {
    @Tag static var increase: Self
    @Tag static var decrease: Self
}

Solch ein Tag kann dann als Teil des @Test-Makros als Parameter übergeben werden. Dazu ruft man die tags(_:)-Funktion auf und übergibt den gewünschten Tag.

@Suite("Calculator Tests") struct TS_Swift_TestingTests {
    @Test("Addition Test", .tags(.increase)) func addition() { ... }
    
    @Test("Subtraction Test", .tags(.decrease)) func subtraction() { ... }
}

Obwohl beide Tests in derselben Suite definiert sind, wurden sie mithilfe von Tags jeweils noch einer weiteren zusätzlichen Kategorie zugeordnet. Im Test Navigator werden alle vorhandenen Tags am Ende der Testliste aufgeführt. Über die Play-Schaltfläche ist es möglich, sodann alle Tests, die Teil des zugehörigen Tags sind, auszuführen.

Die gesetzten Tags tauchen am Ende der Testliste auf.
Die gesetzten Tags tauchen am Ende der Testliste auf.

Zusätzlich lässt sich die Darstellung der Tests noch nach Tags sortieren. Dazu genügt ein Klick auf die Group by Tag-Schaltfläche am oberen rechten Rand des Test Navigators.

Auf Wunsch gruppiert man die Testliste anhand der gesetzten Tags.
Auf Wunsch gruppiert man die Testliste anhand der gesetzten Tags.

Im Übrigen lassen sich nicht nur Tests mit Tags versehen. Auf die gleiche Art kann auch eine ganze Suite (und damit alle in ihr enthaltenen Tests) einen Tag erhalten, indem man bei der @Suite-Deklaration den gewünschten Tag als Parameter übergibt.

Ausblick

Swift Testing ist eine gelungene Ergänzung in Apples Entwickler-Portfolio. Nach SwiftUI und SwiftData geht so ein weiteres Framework an den Start, das dank exzessiven Gebrauchs von Makros eine komfortable und übersichtliche Grundlage zum Schreiben von Unit-Tests liefert. Umso schade ist es, dass – wenigstens bisher – ein Support für Performance- und UI-Tests fehlt.

Neben den aufgezeigten Grundlagen bietet Swift Testing auch noch weitere Funktionen. So lassen sich Tests (beispielsweise aufgrund bekannter Bugs) temporär deaktivieren, ohne sie dafür auskommentieren zu müssen. Auch können Bedingungen für die Ausführung von Tests festgelegt werden. Das ist beispielsweise bei Multiplatform-Projekten relevant, in denen manche Funktionen nicht in allen Targets nutzbar sind. Und mithilfe des arguments-Parameters lassen sich Tests mehrmals mit verschiedenen Werten durchlaufen.

Swift Testing legt damit einen gelungenen Start hin. Ich bin neugierig zu sehen, wie Apple das Framework in den nächsten Jahren ausbauen und erweitern wird. Ich persönlich werde meine Unit-Tests mit dem Release von Xcode 16 ausschließlich noch mit Swift Testing umsetzen (und bin in diesem Zuge sehr gespannt darauf, welche Erfahrungen und Erkenntnisse sich daraus noch ergeben werden).