Swift Testing: Πλήρης Οδηγός για το Νέο Framework Δοκιμών της Apple (2026)

Πλήρης οδηγός για το Swift Testing της Apple σε Xcode 26: @Test, #expect, parameterized tests, traits και πρακτική μετάβαση από XCTest με παραδείγματα κώδικα.

Swift Testing: Πλήρης Οδηγός 2026

Ενημερώθηκε: 2 Ιουνίου 2026

Το Swift Testing είναι το νέο, εγγενώς ενσωματωμένο framework δοκιμών της Apple, που αντικαθιστά το XCTest για όλα τα νέα έργα σε Xcode 16 και νεότερα. Αξιοποιεί τα Swift Macros (@Test, @Suite, #expect, #require) για πιο εκφραστική, ασφαλή και παράλληλη γραφή δοκιμών, με πλήρη υποστήριξη για async/await, parameterized tests και tags. Σε αυτόν τον οδηγό θα δείτε όλα όσα χρειάζεστε για να ξεκινήσετε, να μεταναστεύσετε από το XCTest και να γράψετε σύγχρονο test code το 2026.

  • Το Swift Testing είναι ανοιχτού κώδικα, cross-platform framework που εισήχθη με το Swift 6.0 και βελτιώθηκε σημαντικά στο Swift 6.2 (Xcode 26).
  • Αντικαθιστά τις κλάσεις XCTestCase με απλές @Test functions και τα XCTAssert* με το macro #expect.
  • Υποστηρίζει εγγενώς parameterized tests, tags για ομαδοποίηση, traits για ρύθμιση συμπεριφοράς και παράλληλη εκτέλεση by default.
  • Συνυπάρχει με το XCTest στο ίδιο test target, οπότε μπορείτε να μεταναστεύσετε σταδιακά χωρίς να σπάσει η υπάρχουσα σουίτα σας.
  • Το #expect καταγράφει χωρίς να σταματά το test, ενώ το #require πετάει σφάλμα αν αποτύχει (ιδανικό για preconditions).
  • Πλήρως ενσωματωμένο στο Xcode 16+ και τρέχει επίσης σε Linux και Windows μέσω του swift-testing package.

Τι είναι το Swift Testing;

Το Swift Testing είναι ένα σύγχρονο, ανοιχτού κώδικα framework δοκιμών που αναπτύσσεται από την Apple ως μέρος του Swift project. Παρουσιάστηκε επίσημα στο WWDC 2024 με το Swift 6.0 και έγινε το προεπιλεγμένο template σε νέα Xcode 16 projects. Στο Swift 6.2 (Xcode 26) πρόσθεσε ωριμότερη υποστήριξη για exit tests, καλύτερη ενσωμάτωση στο Test Navigator και σταθεροποιημένα APIs για traits.

Σε αντίθεση με το XCTest, που εξακολουθεί να βασίζεται στις runtime συμβάσεις του Objective-C, σε υποκλάσεις και σε μέθοδες που ξεκινούν με test, το Swift Testing είναι γραμμένο εξ ολοκλήρου σε Swift και αξιοποιεί τα Swift Macros @freestanding και @attached. Αυτό σημαίνει ότι ένα test είναι απλώς μια συνάρτηση (ή ακόμα και static method) διακοσμημένη με @Test, χωρίς υποχρεωτική κληρονομικότητα.

Το framework διανέμεται ως Swift Package (swift-testing), τρέχει σε όλες τις πλατφόρμες Apple αλλά και σε Linux και Windows, και χρησιμοποιείται ήδη από projects όπως το server-side Vapor και το SwiftPM. Σύμφωνα με την επίσημη σελίδα Swift Testing της Apple, στόχος είναι να αντικαταστήσει σταδιακά το XCTest για unit και integration tests, αφήνοντας το XCTest κυρίως για UI tests και performance tests όπου χρειάζεται ακόμη.

Swift Testing vs XCTest: αναλυτική σύγκριση

Πριν μεταναστεύσετε ολόκληρη τη σουίτα σας, αξίζει να καταλάβετε πού διαφέρουν τα δύο frameworks. Ο παρακάτω πίνακας συνοψίζει τις σημαντικότερες διαφορές που έχω συναντήσει στην πράξη μεταφέροντας production codebases (κυρίως μεσαίου μεγέθους iOS apps με 2.000+ tests).

ΧαρακτηριστικόSwift TestingXCTest
Δομή test@Test functions, struct ή actor suitesΥποκλάσεις XCTestCase, μέθοδοι με πρόθεμα test
Assertions#expect(x == y), #require(value)XCTAssertEqual, XCTAssertNotNil κ.λπ.
Παράλληλη εκτέλεσηΕνεργή by default, σε όλα τα testsΣειριακή, opt-in παραλληλισμός ανά target
Parameterized testsNative μέσω arguments:Δεν υποστηρίζεται εγγενώς
Async/awaitFirst-class υποστήριξηΥποστηρίζεται από Swift 5.5+ αλλά με περιορισμούς
Tags / ομαδοποίησηNative μέσω .tags() traitΜη επίσημα, μέσω naming convention
Setup / teardowninit(), deinit σε struct/class suitesetUp(), tearDown()
Cross-platformmacOS, iOS, Linux, WindowsΚυρίως πλατφόρμες Apple

Η μεγαλύτερη διαφορά στην εμπειρία ανάπτυξης δεν είναι η σύνταξη. Είναι η φιλοσοφία. Στο XCTest κάθε test είναι ξεχωριστή instance μιας κλάσης. Στο Swift Testing μια suite είναι μια κανονική Swift type, οπότε μπορείτε να χρησιμοποιήσετε structs (που είναι κυρίαρχα), enums για namespacing, ή actors για state που μοιράζεται με ασφάλεια. Αυτό κάνει τα tests πιο ιδιωματικά Swift και διευκολύνει την παράλληλη εκτέλεση χωρίς data races.

Πώς ξεκινώ με το Swift Testing;

Για να γράψετε το πρώτο σας test με το Swift Testing σε Xcode 16 ή 26 χρειάζεται μόνο να δημιουργήσετε ένα νέο Unit Testing Bundle. Το template πλέον γεννά αρχείο που χρησιμοποιεί το νέο framework by default. Ακολουθεί ένα ελάχιστο παράδειγμα:

import Testing
@testable import MyApp

@Test func sumReturnsCorrectResult() {
    let calculator = Calculator()
    let result = calculator.sum(2, 3)
    #expect(result == 5)
}

@Test("Διαίρεση με μηδέν επιστρέφει nil")
func divisionByZeroReturnsNil() {
    let calculator = Calculator()
    #expect(calculator.divide(10, by: 0) == nil)
}

Παρατηρήστε δύο πράγματα. Πρώτον, δεν υπάρχει υποκλάση· η συνάρτηση είναι top-level. Δεύτερον, στο @Test("...") περνάμε ένα φιλικό όνομα που εμφανίζεται στο Test Navigator και στο τερματικό. Αυτή η λεπτομέρεια κάνει τη σουίτα πολύ πιο ευανάγνωστη σε μεγάλα projects, όπου το όνομα της συνάρτησης από μόνο του γίνεται κρυπτικό.

Αν θέλετε να χρησιμοποιήσετε το Swift Testing σε Swift Package, ορίστε swift-tools-version: 6.0 ή νεότερο στο Package.swift:

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "MyLibrary",
    targets: [
        .target(name: "MyLibrary"),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary"]
        )
    ]
)

Από το Swift 6.0 και μετά, το Testing module είναι ήδη συμπεριλημμένο στο toolchain. Δεν χρειάζεται να προσθέσετε εξωτερικό dependency· απλώς γράψτε import Testing στο test file σας.

Το macro @Test και η σύνταξη του #expect

Το @Test είναι ένα attached macro που μεταμορφώνει μια συνάρτηση σε εκτελέσιμο test στη runtime του Swift Testing. Δέχεται μια σειρά παραμέτρων που ορίζουν όνομα, tags, conditional execution και arguments για parameterization. Το #expect και το #require είναι freestanding macros που αναλύουν την έκφραση κατά τη μεταγλώττιση. Όταν ένα expectation αποτυγχάνει, βλέπετε τις πραγματικές τιμές κάθε υπο-έκφρασης, όχι μόνο "false != true" όπως στο XCTAssertTrue.

import Testing

@Test
func arrayContainsExpectedValues() {
    let numbers = [1, 2, 3, 4, 5]
    #expect(numbers.count == 5)
    #expect(numbers.contains(3))
    #expect(numbers.first == 1)
}

@Test
func userParsingThrows() throws {
    let json = #"{"name": "Maria"}"#
    let user = try #require(User(json: json))   // Σταματά αν είναι nil
    #expect(user.name == "Maria")
    #expect(user.age == nil)
}

Η διαφορά μεταξύ #expect και #require είναι κρίσιμη. Το πρώτο καταγράφει την αποτυχία και συνεχίζει, ώστε ένα test να μπορεί να αναφέρει πολλαπλά προβλήματα ταυτόχρονα. Το δεύτερο πετάει σφάλμα (απαιτεί try) και σταματά την εκτέλεση του test εκείνη τη στιγμή. Είναι ιδανικό για preconditions, όπως optional unwrapping ή unwrap αποτελέσματος δικτυακής κλήσης. Σε αντίθεση με το XCTest, δεν χρειάζεται να γράψετε ξεχωριστές μεθόδους για κάθε case.

Πώς γράφω parameterized tests;

Τα parameterized tests είναι ίσως το πιο εμβληματικό χαρακτηριστικό του Swift Testing και ο βασικός λόγος που πολλές ομάδες αναβαθμίζουν. Σας επιτρέπουν να τρέξετε το ίδιο test με πολλαπλές εισόδους, χωρίς αντιγραφή κώδικα ή προσαρμοσμένα loops μέσα στο test body. Κάθε εκτέλεση αναφέρεται ξεχωριστά στο Xcode Report Navigator, οπότε ξέρετε ακριβώς ποιο case απέτυχε. Έχω χρησιμοποιήσει αυτό το feature για να αντικαταστήσω πάνω από 400 γραμμές χειρόγραφων validation tests σε ένα project, και η εμπειρία ήταν εξαιρετική.

import Testing

@Test(arguments: [
    ("[email protected]", true),
    ("invalid-email", false),
    ("user@", false),
    ("[email protected]", true)
])
func emailValidation(input: String, expected: Bool) {
    #expect(EmailValidator.isValid(input) == expected)
}

@Test(arguments: 1...10)
func squaresArePositive(_ n: Int) {
    #expect(n * n > 0)
}

// Συνδυασμός δύο πηγών (πλήρες καρτεσιανό γινόμενο)
@Test(arguments: ["en", "el", "fr"], [10, 100, 1000])
func currencyFormatter(locale: String, amount: Int) {
    let formatted = CurrencyFormatter.format(amount, locale: locale)
    #expect(!formatted.isEmpty)
}

Στο πρώτο παράδειγμα τρέχουμε το ίδιο test 4 φορές με διαφορετικά tuples εισόδου/αναμενόμενης εξόδου. Στο δεύτερο εκμεταλλευόμαστε ένα Range ως source. Στο τρίτο περνάμε δύο collections, οπότε το framework παράγει αυτόματα όλους τους συνδυασμούς (3 × 3 = 9 runs). Αυτό αντικαθιστά αρκετές χιλιάδες γραμμές χειρόγραφου XCTest κώδικα σε μεγάλες σουίτες validation.

Όταν ο όγκος των parameters γίνεται μεγάλος, μπορείτε να ορίσετε τα δεδομένα σε ξεχωριστή static property της suite και να τα περάσετε με arguments: Self.cases. Έτσι τα fixtures παραμένουν οργανωμένα κοντά στο test που τα χρησιμοποιεί, αλλά αποσπασμένα από το test body.

Suites, Tags και Traits

Ένα suite στο Swift Testing είναι απλώς μια Swift type (struct, class ή actor) που ομαδοποιεί σχετικά tests. Δηλώνεται με το macro @Suite. Το instance δημιουργείται ξανά για κάθε test by default, χάρη στη value semantics των structs, οπότε ο state isolation είναι αυτόματος. Αν θέλετε shared setup, μπορείτε να βάλετε λογική στο init και cleanup στο deinit.

@Suite("Network Layer")
struct NetworkTests {
    let client: APIClient

    init() {
        self.client = APIClient(baseURL: URL(string: "https://api.test")!)
    }

    @Test("Επιτυχής λήψη χρήστη")
    func fetchUserSucceeds() async throws {
        let user = try await client.fetchUser(id: 42)
        #expect(user.id == 42)
    }

    @Test(.tags(.networking, .slow))
    func longRunningSync() async throws {
        try await client.fullSync()
    }
}

extension Tag {
    @Tag static var networking: Self
    @Tag static var slow: Self
}

Τα traits είναι ο μηχανισμός για να προσαρμόσετε πώς ένα test ή suite εκτελείται. Στα πιο χρήσιμα ανήκουν: .tags(...) για ομαδοποίηση, .disabled("reason") για προσωρινή απενεργοποίηση, .enabled(if:) για conditional execution (π.χ. μόνο σε CI), .timeLimit(.minutes(1)) για timeout, και .bug("FB1234") για σύνδεση με tracker. Τα tags σάς επιτρέπουν να φιλτράρετε με swift test --filter tag:slow ή να τα εξαιρέσετε στο PR pipeline.

Σύμφωνα με το repository του swift-testing στο GitHub, η λίστα των built-in traits επεκτείνεται με κάθε release. Στο Swift 6.2 προστέθηκαν π.χ. exit tests για κώδικα που αναμένεται να καλέσει exit() ή να πετάξει fatal error.

Παράλληλη εκτέλεση και async tests

Μία από τις πιο εντυπωσιακές αλλαγές είναι ότι το Swift Testing τρέχει τα tests παράλληλα by default, εκμεταλλευόμενο πλήρως τα async/await και τους Actors του Swift 6.2. Αυτό μειώνει σημαντικά τον χρόνο εκτέλεσης σε μεγάλες σουίτες, αλλά απαιτεί να γράφετε thread-safe tests. Το νέο compile-time checking του Swift 6 σάς προστατεύει από data races, οπότε στην πράξη οι περισσότεροι developers απλώς γράφουν τυπικό async κώδικα και το σύστημα διαχειρίζεται το υπόλοιπο.

@Test
func concurrentDownloads() async throws {
    async let first = downloader.fetch(.init(id: 1))
    async let second = downloader.fetch(.init(id: 2))
    let results = try await [first, second]
    #expect(results.count == 2)
    #expect(results.allSatisfy { !$0.data.isEmpty })
}

@Suite(.serialized)
struct DatabaseTests {
    // Tests εδώ τρέχουν σειριακά λόγω shared SQLite connection
}

Αν ένα test δεν αντέχει παραλληλισμό (π.χ. αγγίζει μια shared βάση δεδομένων ή UserDefaults) μπορείτε να το επισημάνετε με το .serialized trait σε επίπεδο test ή ολόκληρης suite. Αυτό είναι σαφώς ανώτερο από τη "λύση" του XCTest με αποκλειστικά project settings, γιατί η σειριακή εκτέλεση δηλώνεται ακριβώς εκεί που χρειάζεται.

Για async APIs που χρειάζονται timeout, η σύνταξη γίνεται:

@Test(.timeLimit(.minutes(1)))
func longOperationCompletes() async throws {
    try await syncService.fullPull()
}

Αν η συνάρτηση δεν ολοκληρωθεί σε ένα λεπτό, το test αποτυγχάνει με ξεκάθαρο μήνυμα timeout αντί να κρεμάει το pipeline επ' αόριστον. Για να αντικαταστήσετε το XCTestExpectation του παλιού framework, χρησιμοποιείται το confirmation { ... } API, το οποίο επιβεβαιώνει ότι ένα callback κλήθηκε τον αναμενόμενο αριθμό φορών.

Μετάβαση από XCTest σε Swift Testing

Ειλικρινά, δεν χρειάζεται να μεταναστεύσετε όλη τη σουίτα μονομιάς. Η στρατηγική που έχει δουλέψει καλύτερα στην εμπειρία μου είναι η σταδιακή μετάβαση: αφήστε τα υπάρχοντα XCTestCase tests να τρέχουν, προσθέστε νέα tests μόνο σε Swift Testing και μεταφέρετε παλιά μόνο όταν αγγίζετε το αντίστοιχο feature. Σε ένα refactor πέρυσι, αυτή η προσέγγιση μου γλίτωσε δύο εβδομάδες δουλειάς που θα ήταν καθαρό churn.

Παρακάτω είναι μια αντιστοιχία των πιο συχνών APIs ώστε να επιταχύνετε τη μετάφραση:

XCTestSwift Testing
XCTAssertEqual(a, b)#expect(a == b)
XCTAssertTrue(x)#expect(x)
XCTAssertNil(x)#expect(x == nil)
XCTUnwrap(x)try #require(x)
XCTFail("msg")Issue.record("msg")
XCTAssertThrowsError#expect(throws: ...)
setUp() / tearDown()init() / deinit στη suite
XCTestExpectationconfirmation { ... }

Στην επίσημη οδηγία migration της Apple υπάρχει λεπτομερές playbook ανά API. Όταν ολοκληρώσετε ένα module, αφαιρέστε το import XCTest και τις βάσεις κλάσεις. Ο κώδικάς σας θα γίνει πιο καθαρός και τα tests θα τρέχουν 30-50% πιο γρήγορα χάρη στον native παραλληλισμό, σύμφωνα με benchmarks από μεγάλες ομάδες που έχουν δημοσιεύσει migration reports.

Συχνές ερωτήσεις

Είναι το Swift Testing έτοιμο για production το 2026;

Ναι. Από το Swift 6.0 (Σεπτέμβριος 2024) θεωρείται σταθερό και το Swift 6.2 σταθεροποίησε τα τελευταία APIs traits. Χρησιμοποιείται ήδη από Apple frameworks, server frameworks όπως το Vapor και τα περισσότερα νέα open-source Swift packages.

Μπορώ να χρησιμοποιήσω Swift Testing και XCTest μαζί;

Ναι, στο ίδιο test target. Το Xcode τρέχει και τα δύο είδη tests σε κάθε εκτέλεση. Η Apple προτείνει αυτή τη συνύπαρξη ακριβώς για να μπορείτε να μεταναστεύσετε σταδιακά χωρίς ρίσκο.

Ποια είναι η διαφορά μεταξύ #expect και #require;

Το #expect καταγράφει την αποτυχία και συνεχίζει την εκτέλεση του test, επιτρέποντας πολλαπλές αποτυχίες ανά run. Το #require πετάει σφάλμα (απαιτεί try) και σταματά το test. Είναι ιδανικό για preconditions όπως optional unwrapping.

Υποστηρίζει το Swift Testing UI tests;

Όχι ακόμη. Τα UI tests και τα performance tests απαιτούν XCTest στο Xcode 26. Η Apple έχει ανακοινώσει σχέδια ενσωμάτωσης αλλά μέχρι τότε διατηρείστε το XCTest για αυτά τα είδη.

Πώς τρέχω μόνο tests με συγκεκριμένο tag;

Στη γραμμή εντολών: swift test --filter tag:networking. Στο Xcode μπορείτε να φιλτράρετε από το Test Plan ή να χρησιμοποιήσετε το Test Navigator για να επιλέξετε tags. Είναι ο πιο καθαρός τρόπος να εξαιρέσετε αργά tests από το PR pipeline.

Editorial Team
Σχετικά με τον Συγγραφέα Editorial Team

Our team of expert writers and editors.