Swift Testing: framework zastępujący XCTest i co pozostaje w XCTest
XCTest, framework testowy Apple od 2013 roku, jest zastępowany przez Swift Testing. Apple nie używa jeszcze określenia „przestarzały” (XCTest nadal jest dostarczany, nadal działa, nadal obsługuje automatyzację UI i testy wydajności), jednak każda sesja Apple, każdy wątek na Swift Forums i każdy nowy projekt przykładowy od WWDC 2024 kieruje kod Swift do Swift Testing. Framework dostarczany jest z Xcode 16+ i Swift 61, działa na macOS, iOS, watchOS, tvOS, visionOS oraz Linux2 i prezentuje odmienny model myślowy: testy są funkcjami, suity są typami, konfiguracja to cechy, a równoległość jest domyślna.
Migracja jest przyrostowa. Oficjalna dokumentacja Apple wprost wspiera uruchamianie obu frameworków obok siebie w tym samym celu kompilacji3, co oznacza, że właściwym podejściem dla nietrywialnej bazy kodu nie jest „przepisać wszystko”, lecz „pisać nowe testy w Swift Testing i migrować stare przy okazji ich edycji”. Wpis omawia powierzchnię API, wskazuje granicę migracji (co pozostaje w XCTest) oraz przedstawia słownik cech, który zastępuje konfigurację opartą na hierarchii klas znaną z XCTest.
TL;DR
- Swift Testing zastępuje oparty na klasach i ciągach znaków model XCTest makrami:
@Test,@Suite,#expect(...)oraz#require(...)4. - Testy są wolnymi funkcjami lub metodami w typach; suity to dowolne typy zawierające funkcje testowe (
@Suitejest wymagane wyłącznie przy określaniu nazwy wyświetlanej lub cechy). - Konfiguracja przenosi się z hierarchii klas i konwencji nazewnictwa na cechy:
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)5. - Testy parametryzowane przez
@Test(arguments:)produkują jeden nadrzędny wynik testu z jednym dzieckiem na argument; równoległość domyślna6. - Migracja odbywa się równolegle, nie jednorazowo. Automatyzacja UI (
XCUIApplication) i testowanie wydajności (XCTMetric) pozostają w XCTest, ponieważ Swift Testing ich nie obejmuje3. - WWDC 2025 dodało niestandardowe załączniki (artefakty testowe) oraz testy wyjścia (weryfikujące kod, którego oczekuje się, że zakończy działanie w określonych warunkach)7.
Dlaczego Swift Testing istnieje
Bolączki XCTest są dobrze znane każdemu, kto utrzymywał dużą suitę testów iOS:
Asercje oparte na ciągach znaków. XCTAssertEqual(a, b, "message") nie przechwytuje oryginalnego wyrażenia Swift. Gdy asercja zawodzi, komunikat błędu pokazuje wartości w czasie wykonania, lecz nie wskazuje, które wyrażenie je wyprodukowało. Debugowanie wymaga czytania źródła testu i rekonstruowania kontekstu.
Konfiguracja oparta na klasach. Setup, teardown i współdzielony stan wymagają dziedziczenia po XCTestCase. Wyłączenie testu wymaga albo zmiany jego nazwy (aby wzorzec wykrywania go pominął), albo opakowania w #if DEBUG. Wybór, które testy mają działać, wymaga starannego nazewnictwa.
Niezgodność z współbieżnością. XCTest powstał przed Swift Concurrency. XCTestExpectation wraz z wait(for:timeout:) to przestarzały pomost do kodu asynchronicznego; wzorzec jest rozwlekły i podatny na błędy. Nowoczesny kod Swift używa async/await; idiomy XCTest wydają się anachroniczne.
Słaba parametryzacja. XCTest nie ma natywnego wsparcia dla testów parametryzowanych. Programiści symulują je pętlami for wewnątrz metod testowych (co produkuje jeden wynik testu dla wielu przypadków) albo piszą generatory kodu (co produkuje N metod testowych z szablonu).
Swift Testing odpowiada na każdy z tych problemów:
import Testing
@Test func userInitializesWithDefaults() {
let user = User()
#expect(user.name == "Anonymous")
#expect(user.preferences.isEmpty)
}
#expect przechwytuje pełne wyrażenie. Gdy asercja zawodzi, wynik testu pokazuje, że user.name == "Anonymous" zawiodło, ponieważ user.name miało wartość "Guest". Framework wykonuje pracę odczytania wyrażenia, ponieważ makro widzi drzewo składniowe. Żadnego zgadywania w stylu „oczekiwano X, otrzymano Y”.
Makra
Pracę wykonują cztery makra.
@Test i @Suite
@Test oznacza funkcję jako test. Funkcja może znajdować się na poziomie pliku, w strukturze, w aktorze, w wyliczeniu — wszędzie tam, gdzie funkcja może istnieć w Swift. Framework wykrywa funkcje oznaczone @Test i je uruchamia.
@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 jawnie oznacza typ jako suitę testową. Oznaczenie jest dorozumiane, gdy typ zawiera funkcje @Test; jawne @Suite jest wymagane wyłącznie przy dodawaniu niestandardowej nazwy wyświetlanej lub stosowaniu cechy na poziomie suity4.
@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
@Test func loginSucceeds() async throws { ... }
@Test func loginFails() async throws { ... }
}
#expect i #require
#expect to standardowa asercja. Test kontynuuje działanie, jeśli oczekiwanie zawiedzie (niepowodzenie zostaje zarejestrowane; kolejne asercje nadal się wykonują). #require to wariant kończący szybko: jeśli wymaganie zawiedzie, test natychmiast się zatrzymuje. Należy używać #require dla warunków wstępnych, których niespełnienie wygenerowałoby bezsensowne błędy w kolejnych asercjach.
@Test func userPreferencesLoad() async throws {
let user = try #require(await store.fetchUser(id: 42))
// Jeśli fetchUser zwraca nil, test zatrzymuje się tutaj. Następna linia
// nigdy nie wykonuje się na rozpakowanym opcjonalu.
#expect(user.preferences["theme"] == "dark")
#expect(user.preferences.count >= 1)
}
try przed #require ma znaczenie: #require zwraca rozpakowaną wartość, gdy wymaganie jest spełnione (w tym przypadku nieopcjonalny User), albo rzuca wyjątek, gdy nie jest. Funkcja testowa musi być oznaczona throws, aby używać #require do rozpakowywania.
Testy parametryzowane
@Test(arguments:) uruchamia funkcję testową raz dla każdego argumentu. Framework raportuje każdy argument jako test podrzędny, a test nadrzędny pełni rolę zbiorczą.
@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)
}
Framework uruchamia cztery argumenty równolegle domyślnie. Komunikaty o niepowodzeniu testu identyfikują, która krotka argumentów zawiodła, więc debugowanie niepowodzenia parametryzowanego nie wymaga uruchamiania testu w izolacji.
Dla argumentów będących niezależnymi kolekcjami, @Test(arguments: arrayA, arrayB) uruchamia iloczyn kartezjański (każdą kombinację). Dla par sekwencyjnych należy użyć zip(arrayA, arrayB) i przekazać uzyskaną sekwencję.
Cechy: konfiguracja jako wartości pierwszej klasy
XCTest używa hierarchii klas, konwencji nazewnictwa i planów testów Xcode do konfiguracji testów. Swift Testing używa cech (traits): wartości stosowanych do deklaracji @Test i @Suite5:
@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 {
// Wszystkie testy w tej suicie wykonują się sekwencyjnie.
// Przydatne, gdy testy współdzielą fixture bazy danych.
@Test func insertUser() { ... }
@Test func updateUser() { ... }
@Test func deleteUser() { ... }
}
Słownik cech obejmuje przypadki, dla których XCTest wymagał niestandardowej infrastruktury:
.enabled(if:)i.disabled(_:). Warunkowe włączanie z wyrażeniem czasu wykonania i opcjonalnym powodem. Zastępuje bloki#if DEBUGi sztuczki ze zmianą nazwy..serialized. Oznacza suitę lub test parametryzowany jako sekwencyjny. Dziedziczne: serializowana suita wymusza sekwencyjność wszystkich testów podrzędnych6..timeLimit(...). Limit czasu na test. Test zostaje zakończony i zaraportowany jako nieudany, jeśli przekroczy limit..tags(...). Tagi do filtrowania. Reguły uwzględniania/wykluczania tagów konfiguruje się w planie testów Xcode; plan uruchamia się przezxcodebuild test -testPlan <name>. Warto zauważyć, że-only-testing:filtruje po identyfikatorze testu (cel, suita lub funkcja), nie po tagu..bug(...). Łączy test z identyfikatorem błędu wraz z powodem. Łącze pojawia się w raporcie testowym i (dla testów znanych jako nieudane) zapobiega blokowaniu CI przez niepowodzenie.
Wspierane są również niestandardowe cechy. Aplikacja może definiować własne cechy dla potrzeb specyficznych dla projektu (np. .requiresKeychain, .skipIfOffline).
Równoległość jest domyślna
Swift Testing uruchamia testy równolegle domyślnie, włączając w to argumenty parametryzowane. Domyślne zachowanie ma znaczenie, ponieważ większość suit testowych rośnie z czasem, a równoległość to różnica między 30-sekundowym a 5-minutowym uruchomieniem testów. XCTest wymagał jawnego włączenia (przełącznik równoległości testów w planie testów); Swift Testing odwraca domyślne zachowanie.
Wynika z tego konsekwencja: testy muszą być niezależne. Współdzielony stan (fixture bazy danych, zamockowany system plików, singleton na poziomie aplikacji) musi być albo izolowany dla każdego testu (każdy test otrzymuje świeży fixture), albo oznaczony .serialized (suita wykonuje się sekwencyjnie). Testy mutujące stan globalny i zakładające wykonanie sekwencyjne to błędy, które Swift Testing ujawnia szybciej niż XCTest.
Migracja: równolegle, nie jednorazowo
Oficjalne stanowisko Apple w sprawie migracji jest przyrostowe3:
- Oba frameworki współistnieją w tym samym celu testowym.
- Nowe testy można pisać w Swift Testing od razu.
- Stare testy XCTest nadal działają bez zmian.
- Migracja testów XCTest do Swift Testing wtedy, gdy jest to wygodne (edytowany plik, nowa funkcja, refactor).
Dwa przypadki pozostają w XCTest na czas nieokreślony:
Testy automatyzacji UI. XCUIApplication, XCUIElement, XCUIElementQuery, cała powierzchnia testowania UI, są wyłącznie XCTest3. Swift Testing nie zapewnia zamiennika. Testy UI pozostają w XCTest, dopóki Apple nie rozszerzy zakresu Swift Testing.
Testy wydajności. XCTMetric, measure(metrics:), baseline’y wydajnościowe, również wyłącznie XCTest3. Swift Testing nie ma jeszcze odpowiednika.
Dla wszystkiego pozostałego (testy jednostkowe, testy integracyjne, testy asynchroniczne, testy parametryzowane) Swift Testing jest rekomendowanym domyślnym wyborem dla nowego kodu.
Co dodało WWDC 2025
Dwa znaczące dodatki do Swift Testing na WWDC 20257:
Niestandardowe załączniki. Test może dołączyć dowolne dane Attachable do swojego wyniku w celu klasyfikacji niepowodzeń. Obrazy nieudanych zrzutów ekranu, logi diagnostyczne, wygenerowane pliki wejściowe. Załączniki pojawiają się w raporcie testowym Xcode oraz w artefaktach CI. Attachment.record(_:named:sourceLocation:) API w Sources/Testing/Attachments/Attachment.swift definiuje powierzchnię; wartości zgodne z Attachable (Data, String, typy obrazów przez opcjonalną zgodność) są akceptowane.
@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)
}
Testy wyjścia. Testy weryfikujące kod, który ma się zakończyć (wywołać exit(), rzucić błąd krytyczny, wywołać niepowodzenie warunku wstępnego). XCTest nie miał czystego sposobu na ich testowanie; testy wyjścia Swift Testing uruchamiają kod w procesie potomnym i weryfikują zachowanie zakończenia.
@Test func parserExitsOnMalformedInput() async {
await #expect(processExitsWith: .failure) {
Parser.parse(malformedInput)
}
}
Oba dodatki są pragmatyczne. Niestandardowe załączniki rozwiązują rzeczywisty problem (debugowanie niestabilnych niepowodzeń CI); testy wyjścia obejmują autentyczną lukę w powierzchni testów jednostkowych.
Kiedy XCTest nadal jest właściwym wyborem
Trzy przypadki, w których nowy kod nadal powinien być w XCTest:
Test jest testem automatyzacji UI. XCUIApplication.launch, interakcje typu tap-and-swipe, zapytania oparte na identyfikatorach dostępności. Swift Testing nie obejmuje powierzchni automatyzacji UI; nowe testy UI są w XCTest.
Test jest benchmarkiem wydajności. measure(metrics: [XCTClockMetric()]), baseline’y wydajnościowe, pomiary pamięci. Swift Testing nie ma odpowiedników; nowe testy wydajności są w XCTest.
Test musi działać na platformie nieobsługującej Swift Testing. Swift Testing wymaga Swift 6 / Xcode 16+. Testy, które muszą się kompilować przeciwko wcześniejszym toolchainom (matryca CI obejmująca starsze wersje Xcode), wymagają XCTest, dopóki toolchain nie nadrobi zaległości.
Dla testów jednostkowych, integracyjnych, asynchronicznych i parametryzowanych w celach Swift 6+ Swift Testing jest nowoczesnym domyślnym wyborem. Koszt migracji jest realny, ale koszt jej zaniechania kumuluje się; każdy nowy XCTest napisany dziś to dług techniczny.
Powiązanie z workflow agentów
Testy są kontraktem, na podstawie którego działa generowanie kodu wspomagane przez AI. Gdy agent pisze kod Swift, testy są tym, czym programista (lub sam agent) weryfikuje poprawność zmiany. Właściwości Swift Testing czynią kod generowany przez agenty łatwiejszym do weryfikacji:
- Przechwytywanie wyrażeń. Niepowodzenie
#expect(user.score == calculator.compute(user))pokazuje obie strony porównania. Agent czytający niepowodzenie może naprawić błąd bez ponownego czytania pliku testu. - Przypadki parametryzowane. Agent naprawiający regresję wie, które konkretne wejście zawiodło. Naprawa może być ukierunkowana.
- Filtrowanie oparte na cechach. Adnotacja
.tags(.regression)pozwala agentowi uruchomić tylko testy regresji po naprawie; szybsza pętla zwrotna. - Niestandardowe załączniki. Agent debugujący niestabilny test może dołączyć artefakt z nieudanymi danymi wejściowymi i odczytać go przy następnym uruchomieniu.
Wpis o hookach do rozwoju Apple omawia hooki Stop, które uruchamiają testy po każdej edycji Claude Code. Szybsze, bardziej równoległe i bardziej informatywne niepowodzenia Swift Testing czynią tę pętlę na tyle ciasną, że agent może iterować bez ręcznej interwencji.
Co ten wzorzec oznacza dla aplikacji iOS 26+
Trzy wnioski.
-
Domyślnie pisz nowe testy w Swift Testing. Framework jest bardziej ekspresyjny, działa równolegle domyślnie, automatycznie przechwytuje kontekst niepowodzeń i wspiera testy parametryzowane jako koncepcję pierwszej klasy. Istniejący kod XCTest nadal działa; nowy kod używa nowoczesnego domyślnego wyboru.
-
Zachowaj automatyzację UI i testy wydajności w XCTest. Luka w pokryciu Apple, nie wybór migracyjny. Dopóki Swift Testing nie obejmie tych domen, te testy pozostają w XCTest na czas nieokreślony.
-
Używaj cech jako słownika konfiguracji.
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...)obejmują przypadki, dla których XCTest wymagał hierarchii klas, konwencji nazewnictwa i bloków#if DEBUG. Cechy to wartości pierwszej klasy, które się komponują; słownik cech jest tym, co sprawia, że framework wydaje się nowoczesny.
Pełny klaster Apple Ecosystem: typowane App Intents; serwery MCP; pytanie o routing; Foundation Models; rozróżnienie LLM runtime kontra tooling; trzy powierzchnie; wzorzec pojedynczego źródła prawdy; Dwa serwery MCP; hooki dla rozwoju Apple; Live Activities; runtime watchOS; wnętrzności SwiftUI; model myślowy przestrzenny RealityKit; dyscyplina schematu SwiftData; wzorce Liquid Glass; wieloplatformowy shipping; matryca platform; framework Vision; Symbol Effects; inferencja Core ML; Writing Tools API; o czym odmawiam pisać. Hub znajduje się na stronie Apple Ecosystem Series. Szerszego kontekstu iOS-z-agentami-AI dostarcza przewodnik iOS Agent Development.
FAQ
Czy Swift Testing zastępuje XCTest w całości?
Jeszcze nie. XCTest nadal hostuje testy automatyzacji UI (XCUIApplication) oraz testy wydajności (XCTMetric); Swift Testing nie obejmuje tych domen. Dla testów jednostkowych, integracyjnych, asynchronicznych i parametryzowanych Swift Testing jest rekomendowanym domyślnym wyborem dla nowego kodu. Oba frameworki współistnieją w tym samym celu bez konfliktu.
Jaka jest praktyczna różnica między #expect a XCTAssertEqual?
#expect przechwytuje pełne wyrażenie Swift i raportuje obie strony porównania przy niepowodzeniu (np. user.score == calculator.compute(user) zawiodło, ponieważ user.score = 80, a calculator.compute(user) = 100). XCTAssertEqual zna tylko wartości czasu wykonania i opcjonalny ciąg komunikatu. Przechwytywanie wyrażenia sprawia, że niepowodzenia Swift Testing są samo-wyjaśniające bez ponownego czytania źródła testu.
Jak działają testy parametryzowane?
@Test(arguments: [...]) uruchamia funkcję testową raz na każdy argument. Framework raportuje każdy argument jako wynik testu podrzędnego, a test nadrzędny pełni rolę zbiorczą. Krotki mogą przekazywać wiele wartości. Testy parametryzowane działają równolegle domyślnie; należy dodać .serialized, aby wymusić sekwencyjne wykonanie.
Do czego służy #require?
#require to wariant #expect kończący szybko. Należy go używać dla warunków wstępnych, których niespełnienie wygenerowałoby bezsensowne błędy w kolejnych asercjach. try #require(value) zwraca rozpakowaną wartość, gdy warunek jest spełniony, albo rzuca wyjątek, gdy nie, więc reszta testu może bezpośrednio używać rozpakowanej wartości. Funkcja testowa musi być oznaczona throws, aby używać #require do rozpakowywania.
Czy mogę pisać testy Swift Testing w Linuxie?
Tak. Swift Testing jest open-source2 i działa na każdej platformie obsługiwanej przez Swift, w tym Linuxie. Projekty Swift po stronie serwera mogą natychmiast przyjąć Swift Testing. Framework nie jest specyficzny dla platform Apple.
Jak zmigrować istniejącą suitę XCTest?
Rekomendacja Apple jest przyrostowa: pisać nowe testy w Swift Testing, migrować stare przy okazji ich edycji3. Nie istnieje zautomatyzowany konwerter (różnice modelowe są na tyle duże, że automatyzacja byłaby krucha). Właściwym celem migracji są testy jednostkowe i integracyjne; testy UI i testy wydajności pozostają w XCTest na czas nieokreślony.
Bibliografia
-
Apple Developer: Swift Testing. Przegląd Apple dotyczący adopcji Swift Testing wraz z Xcode 16+ i Swift 6. ↩
-
Repozytorium open-source: swiftlang/swift-testing na GitHub. Źródło frameworka, dostępne na licencji Apache License 2.0 wraz ze standardową licencją Swift Project. ↩↩
-
Apple Developer Documentation: Migrating a test from XCTest. Oficjalny przewodnik migracji Apple omawiający równoległe współistnienie oraz wyjątki XCUIApplication / XCTMetric. ↩↩↩↩↩↩
-
Apple Developer Documentation: Swift Testing. Referencja frameworka dla
@Test,@Suite,#expect,#requireoraz słownika cech. ↩↩ -
Apple Developer Documentation: Trait. Protokół definiujący cechy testów, z wbudowanymi cechami obejmującymi
.enabled(if:),.disabled(_:),.serialized,.timeLimit(...),.tags(...),.bug(...). ↩↩ -
Apple Developer: Meet Swift Testing (sesja WWDC 2024 nr 10179) oraz Go further with Swift Testing (sesja WWDC 2024 nr 10195). Sesje wprowadzające omawiające testy parametryzowane i równoległość. ↩↩
-
Materiał z repozytorium open-source dla dodatków WWDC 2025:
Sources/Testing/Attachments/Attachment.swiftdla niestandardowych załączników orazSources/Testing/ExitTests/dla makra testu wyjścia#expect(processExitsWith:). Oba pojawiły się po raz pierwszy na WWDC 2025 i są dostarczane wraz z Xcode 26+. ↩↩