Swift Testing in Xcode 26: Migratiegids van XCTest naar het Nieuwe Framework
Migreer je Swift-tests van XCTest naar Swift Testing in Xcode 26. Praktische voorbeelden van @Test, #expect, parameterized tests en parallelle uitvoering.
Swift Testing is het nieuwe, op macro's gebaseerde testframework van Apple dat sinds Xcode 16 standaard meegeleverd wordt en in Xcode 26 volledig productieklaar is. Het vervangt XCTest niet meteen, maar je kunt vandaag al beide naast elkaar draaien in hetzelfde testdoel. Met de @Test macro, de #expect en #require assertions, eersteklas async/await-ondersteuning en parallelle uitvoering schrijf je tests die korter, leesbaarder en sneller zijn dan hun XCTest-equivalenten.
Swift Testing is sinds Xcode 16 ingebouwd en is in Xcode 26 / Swift 6.2 het aanbevolen framework voor nieuwe testcode.
Tests zijn gewone func's gemarkeerd met @Test. Geen XCTestCase-subklasse, geen test-prefix nodig.
#expect(expression) en #require(expression) vervangen de tientallen XCTAssert*-varianten met één expressieve macro.
Parameterized tests met @Test(arguments:) vervangen handgeschreven loops en geven aparte fouten per inputwaarde.
Swift Testing draait tests standaard parallel in hetzelfde proces. Let op gedeelde state, vooral @MainActor-isolatie.
XCTest en Swift Testing kunnen in hetzelfde target staan. Je hoeft niet alles in één keer te migreren.
Wat is het Swift Testing framework?
Swift Testing is een open-source testframework dat door Apple werd geïntroduceerd op WWDC 2024 en sindsdien is uitgegroeid tot het standaard testframework dat met Xcode 26 wordt geleverd. Het project leeft op github.com/swiftlang/swift-testing en is geïntegreerd in de Swift-toolchain, dus je hoeft geen extra package toe te voegen om het te gebruiken.
Het framework leunt zwaar op Swift-macro's, waardoor de testcode zelf compact blijft terwijl de macro's bij compileertijd diagnostiek en bron-locatie-informatie injecteren. In de praktijk betekent dat: schrijf gewoon #expect(user.isAdmin) en wanneer dat faalt, krijg je in de testnavigator van Xcode 26 niet alleen "expected true, got false", maar ook de daadwerkelijke waarden van elk subexpressie. Eerlijk gezegd is dat verschil (falende assertions die je vertellen wat er werkelijk gebeurde) een van de grootste verbeteringen ten opzichte van XCTest.
Belangrijk om te weten: Swift Testing is niet bedoeld voor UI-tests. Voor XCUIApplication-gebaseerde end-to-end tests blijf je XCTest gebruiken. Voor unit- en integratietests is Swift Testing nu de aanbevolen route.
XCTest vs Swift Testing: de verschillen
Voordat je begint te migreren is het handig om de fundamentele verschillen helder te hebben. De volgende tabel vergelijkt de twee frameworks op de dimensies die er in dagelijks werk toe doen.
Aspect
XCTest
Swift Testing
Test declaratie
func testFoo() in XCTestCase-subklasse
@Test func foo(), losse functie
Assertions
~40 XCTAssert*-varianten
#expect en #require
Parameterized tests
Niet ingebouwd, schrijf zelf een loop
@Test(arguments:) met inputs
Async support
Beschikbaar maar omslachtig
First-class, gewoon async func
Parallelle uitvoering
Per testklasse, in subprocessen
Parallel binnen één proces
Setup / Teardown
setUp() / tearDown()
init() / deinit
Tests groeperen
Per klasse
@Suite op struct, class of actor
UI testing
Ondersteund (XCUIApplication)
Niet ondersteund, blijf XCTest gebruiken
Het belangrijkste praktische gevolg: een @Test-functie heeft geen self van een testklasse nodig en kan dus losstaan. Suite-types worden voor elke test opnieuw geïnstantieerd, dus state lekt niet tussen tests. Een veelvoorkomende valkuil in XCTest is daarmee verleden tijd.
Je eerste Swift Testing test schrijven
De minimale Swift Testing test bestaat uit een import Testing en een @Test-gemarkeerde functie. Geen klasse, geen prefix:
import Testing
@testable import MijnPakket
@Test func gebruikerNaamWordtGeformatteerd() {
let gebruiker = Gebruiker(voornaam: "Eva", achternaam: "de Vries")
#expect(gebruiker.weergavenaam == "Eva de Vries")
}
Vergelijk dit met het XCTest-equivalent:
import XCTest
@testable import MijnPakket
final class GebruikerTests: XCTestCase {
func testGebruikerNaamWordtGeformatteerd() {
let gebruiker = Gebruiker(voornaam: "Eva", achternaam: "de Vries")
XCTAssertEqual(gebruiker.weergavenaam, "Eva de Vries")
}
}
Drie regels minder, geen subklasse, en bij falen toont Xcode 26 zowel gebruiker.weergavenaam als de verwachte string, niet alleen "XCTAssertEqual failed". Voer je tests uit met ⌘U; de testnavigator herkent @Test-functies automatisch.
#expect en #require: assertions zonder afkortingen
XCTest dwingt je tussen ongeveer veertig assertion-functies te kiezen: XCTAssertEqual, XCTAssertTrue, XCTAssertNil, XCTAssertGreaterThan, enzovoort. Swift Testing reduceert dat tot twee macro's:
#expect(expression) controleert de expressie en gaat door als hij faalt. De test wordt als gefaald gemarkeerd, maar de volgende assertions draaien nog.
#require(expression) controleert de expressie en gooit als hij faalt. De test stopt onmiddellijk. Gebruik dit wanneer vervolgcode niet zinvol is zonder dat de assertie slaagt.
De try #require haalt de optional veilig uit; als de items leeg waren, stopt de test daar in plaats van te crashen op een force-unwrap. Wanneer #expect faalt op een complexe expressie zoals #expect(users.filter { $0.age > 18 }.count == 5), toont Xcode de daadwerkelijke gefilterde array. Geen "expected 5, got 3" zonder context meer.
Voor het testen van gegooide fouten gebruik je #expect(throws:):
Losse @Test-functies werken voor kleine projecten, maar zodra je gedeelde setup of een logische groep tests hebt, gebruik je @Suite. Een suite is een struct, class of actor met @Suite erop, en de testen erin worden methodes:
De init draait voor elke test opnieuw, dat is je nieuwe setUp(). Implementeer deinit voor teardown. Doordat het suite-type voor elke test wordt heropgebouwd, kun je nooit per ongeluk state tussen tests delen. Een klassiek XCTest-probleem waarbij vergeten tearDown-code leidt tot vlokkende tests is hiermee uit de lucht.
Hoe schrijf je parameterized tests in Swift Testing?
Een van de grootste praktische winsten ten opzichte van XCTest zijn parameterized tests: één functie die met meerdere inputs draait, waarbij elke input een aparte test wordt in de navigator. Voeg gewoon arguments: toe aan @Test:
Dat draait negen tests (3 × 3 combinaties), elk individueel zichtbaar in de testresultaten. In XCTest schreef je hiervoor een for-loop in één testfunctie en kreeg je bij falen geen idee welke iteratie de boosdoener was. In een vorig project van mij was dit precies de reden waarom een valutabug drie sprints lang onopgemerkt bleef.
Async tests en actor-isolatie
Async functies zijn in Swift Testing eersteklas burgers. Markeer je test gewoon als async en gebruik await zoals je gewend bent:
@Test func ophalenVanGebruiker() async throws {
let api = TestAPI()
let gebruiker = try await api.haalGebruikerOp(id: 42)
#expect(gebruiker.naam == "Eva")
}
Bij het testen van UI-code of @MainActor-geïsoleerde types markeer je de test of de hele suite met @MainActor:
Onder Swift 6.2 in Xcode 26 zijn deze isolaties strikt. Code die per ongeluk over actors heen springt, faalt bij compileertijd in plaats van als flaky test in CI. Dat is een van de redenen waarom de migratie naar Swift Testing samengaat met het oppakken van Swift's moderne concurrency-features uit iOS 26.
Traits: tags, disabled, timeLimit en bug
Traits zijn modifiers die je aan een test of suite hangt. De meest gebruikte:
extension Tag {
@Tag static var langzaam: Self
@Tag static var netwerk: Self
}
@Test(.tags(.netwerk, .langzaam),
.timeLimit(.minutes(1)))
func haaltGroteCatalogusOp() async throws {
let catalogus = try await CatalogusService.haalOp()
#expect(catalogus.items.count > 1000)
}
@Test(.disabled("Wacht op fix voor #4521"))
func tijdelijkUitgezet() { /* ... */ }
@Test(.bug("https://example.com/issue/4521", "Negatieve totalen niet correct"))
func negatiefBedragWordtAfgewezen() {
#expect(throws: OrderError.ongeldigBedrag) {
try Order.maakAan(bedrag: -1)
}
}
Met tags filter je in Xcode (of swift test --filter tag:netwerk) welke tests draaien. Handig voor het scheiden van snelle unit tests en langzame integratietests in CI. De .bug-trait koppelt tests aan issue-tracker URLs, wat helpt bij het terugzoeken van waarom een test überhaupt bestaat.
Parallelle uitvoering en gedeelde state
Swift Testing draait tests standaard parallel binnen één proces, in tegenstelling tot XCTest, dat per testklasse forkt. Dit is veel sneller, maar het betekent dat je tests echt onafhankelijk moeten zijn. Een suite-instantie is per test uniek, dus instance-state is veilig. Gedeelde statische state niet.
Ik liep zelf bij het overzetten van een legacy-suite precies tegen dit muurtje aan: een statische cache die in XCTest "toevallig" werkte, faalde willekeurig zodra Swift Testing tests parallel ging draaien.
Wil je een suite of test sequentieel laten draaien, gebruik dan .serialized:
Voor een SwiftData- of Core Data-test heb je dit vaak nodig omdat een gedeelde test-store anders tegelijk wordt aangeraakt. Lees onze gids over SwiftData model inheritance in iOS 26 voor patronen waarmee je testbare modellen schrijft.
Migratiestrategie: stap voor stap van XCTest
Je hoeft niet alles in één keer te migreren. XCTest en Swift Testing kunnen in hetzelfde testdoel naast elkaar bestaan, en Xcode draait beide. Een werkende migratiestrategie:
Voeg import Testing toe aan een bestaand testdoel. Geen project-instellingen wijzigen, geen Swift Package toevoegen.
Schrijf alle nieuwe tests in Swift Testing. Vanaf vandaag. XCTest-tests blijven gewoon draaien.
Migreer per bestand, niet per test. Een halfgemigreerde klasse is verwarrend. Kies een XCTestCase-subklasse, hernoem hem naar een @Suite-struct, en zet alle test-prefix-methodes om naar @Test-functies in één commit.
Vervang XCTAssert* met #expect. Een eenvoudige zoek-en-vervang: XCTAssertEqual(a, b) wordt #expect(a == b); XCTAssertTrue(x) wordt #expect(x); XCTAssertNil(x) wordt #expect(x == nil).
Vervang setUp / tearDown met init / deinit. Voor async setup gebruik je init() async throws.
Zoek loops in tests en converteer ze naar parameterized tests. Dit is vaak waar Swift Testing het meest aan duidelijkheid wint.
Laat UI-tests in XCTest staan.XCUIApplication werkt niet onder Swift Testing.
De officiële Apple migratiegids voor Swift Testing bevat een uitgebreide mapping-tabel van XCTAssert-functies naar #expect-uitdrukkingen. Bewaar die als referentie tijdens de zoek-en-vervang-fase.
Veelgemaakte fouten bij de migratie
Drie valkuilen die we in echte migraties zijn tegengekomen:
1. Vergeten dat suite-instanties per test worden hergebouwd. In XCTest werd setUp één keer per klasse aangeroepen als je het class-level deed; in Swift Testing draait init voor elke test. Dure setup (zoals een gedeelde in-memory database) moet je expliciet cachen of via een static let-pattern delen, anders krijg je trage tests.
2. Force-unwraps die zich plotseling anders gedragen. XCTest's XCTUnwrap gooit; mensen vervangen het soms door ! in plaats van try #require. Bij parallelle uitvoering crasht een force-unwrap niet alleen die test, maar mogelijk het hele testproces. Gebruik altijd try #require. Ik kwam dit zelf tegen tijdens het shippen van een release: één vergeten force-unwrap, en de hele CI-run sloeg dood.
3. @MainActor-vergeten op UI-tests. Een test die een SwiftUI @Observable-viewmodel aanraakt zonder @MainActor compileert wel, maar faalt onvoorspelbaar onder strict concurrency. Markeer ofwel de hele suite met @MainActor, ofwel de individuele test.
Tot slot: begin vandaag, voltooi geleidelijk
De Swift Testing migratie hoeft geen big-bang-operatie te zijn. Schrijf alle nieuwe tests vanaf nu in Swift Testing, migreer oude bestanden alleen wanneer je ze toch al aanraakt, en laat UI-tests in XCTest. Binnen een paar maanden is het grootste deel van je suite gemigreerd, en je tests zijn ondertussen korter, sneller en informatiever bij falen. Bekijk voor verwante moderne Swift-patronen ook onze complete gids voor Liquid Glass in SwiftUI.
Veelgestelde vragen
Werkt Swift Testing samen met XCTest in hetzelfde project?
Ja. Beide frameworks kunnen in hetzelfde testdoel naast elkaar bestaan. Xcode 26 detecteert @Test-functies en XCTestCase-subklassen automatisch en draait ze in dezelfde testrun. Je hoeft je hele suite niet in één keer te migreren.
Wat is het verschil tussen #expect en XCTAssert?
#expect is één macro die elke booleaanse expressie aanvaardt, terwijl XCTest tientallen specifieke XCTAssert*-functies heeft. Bij falen toont #expect de daadwerkelijke waarden van subexpressies, zodat je exact ziet wat er gebeurde, niet alleen "expected true, got false".
Kan ik Swift Testing gebruiken voor UI-tests?
Nee. XCUIApplication en de bijbehorende UI-test-API's vereisen XCTest. Voor unit- en integratietests gebruik je Swift Testing, voor end-to-end UI-tests blijf je XCTest gebruiken.
Hoe converteer ik setUp en tearDown naar Swift Testing?
Vervang setUp() door de init() van je @Suite-type (gebruik init() async throws voor async setup) en tearDown() door deinit. Omdat het suite-type voor elke test opnieuw wordt geïnstantieerd, krijg je gegarandeerde isolatie zonder zelf state te hoeven resetten.
Welke Swift- en Xcode-versie heb ik nodig voor Swift Testing?
Swift Testing vereist Swift 6.0 of nieuwer en is ingebouwd in Xcode 16 en hoger. Voor de scherpste compileertijd-diagnostiek en volledige actor-isolatie tijdens parallelle uitvoering is Xcode 26 met Swift 6.2 aanbevolen.
Typed throws in Swift 6.2 laten je het exacte fouttype in de functiesignatuur vastleggen. Deze gids behandelt syntaxis, generieke code, async-interactie, prestaties op embedded, en de valkuilen bij publieke API's.
SwiftData in iOS 26 ondersteunt eindelijk echte class inheritance. Leer hoe je model-hiërarchieën opzet, queries optimaliseert met #Index en #Unique, en schema-migraties uitvoert — met werkende codevoorbeelden.
Leer hoe je Liquid Glass implementeert in je SwiftUI-apps voor iOS 26. Van de .glassEffect()-modifier en GlassEffectContainer tot morph-transities, toolbars, tab bars, sheets en best practices — inclusief werkende codevoorbeelden.