Swift Testing: המדריך המלא למסגרת הבדיקות החדשה של Apple (מיגרציה מ-XCTest)

מדריך מקיף ל-Swift Testing, מסגרת הבדיקות החדשה של Apple. תחביר מבוסס מאקרו, בדיקות פרמטריות, מיגרציה הדרגתית מ-XCTest ועבודה עם Swift Concurrency ב-Xcode 26.

Swift Testing: מדריך מלא 2026

עודכן: 9 ביוני, 2026

Swift Testing היא מסגרת הבדיקות המודרנית הרשמית של Apple, שהוכרזה ב-WWDC24 ואומצה כברירת המחדל ב-Xcode 16 ואילך. היא מחליפה את XCTest עבור unit tests בקוד Swift, ומציעה תחביר נקי מבוסס מאקרו (@Test, #expect), הרצה מקבילית כברירת מחדל, ובדיקות פרמטריות מובנות. בניגוד ל-XCTest, אין צורך לרשת ממחלקה: כל פונקציה גלובלית עם הביאור @Test הופכת לבדיקה. במדריך הזה אעבור איתכם על הכל, מהתקנה ועד מיגרציה מ-XCTest, כולל עבודה עם Swift Concurrency.

  • Swift Testing נכלל ב-Xcode 16 ומעלה ותומך ב-Swift 6, iOS 18+, macOS 15+ ו-Linux/Windows דרך Swift Package Manager.
  • בדיקות מסומנות עם @Test במקום שמות פונקציה שמתחילים ב-test, ואסרציות נכתבות עם #expect ו-#require במקום XCTAssert.
  • בדיקות פרמטריות מובנות: @Test(arguments: [1,2,3]) מריץ את אותה בדיקה על כמה ערכים עם דיווח פרטני.
  • הרצה מקבילית של בדיקות מופעלת כברירת מחדל ומאיצה את הסוויטה פי 3-5 בפרויקטים גדולים.
  • אפשר להריץ Swift Testing ו-XCTest זה לצד זה באותו פרויקט, מה שמאפשר מיגרציה הדרגתית.
  • UI Tests ו-Performance Tests עדיין דורשים XCTest. Swift Testing מיועד ל-unit tests בלבד נכון ל-2026.

מה זה Swift Testing ולמה Apple החליפה את XCTest?

Swift Testing היא ספריית open-source שפותחה על ידי Apple (זמינה במאגר swiftlang/swift-testing ב-GitHub) ומוטמעת ישירות ב-toolchain של Swift 6. היא נבנתה מאפס עם Swift Macros, מה שמאפשר תחביר דקלרטיבי ומסרי שגיאה מדויקים. XCTest, לעומת זאת, הוא port של Objective-C מ-2014 ומסתמך על מוסכמות תיוג שמות (שם פונקציה שמתחיל ב-test) ועל מחלקות שיורשות מ-XCTestCase.

ההבדל הבסיסי: ב-XCTest, אסרציה כמו XCTAssertEqual(a, b) מדפיסה רק "expected a, got b". ב-Swift Testing, #expect(a == b) מציג את הביטוי המקורי, את הערכים של כל משתנה שהשתתף בו, ואת ההפרש המדויק, בזכות הרחבת המאקרו בזמן קומפילציה. זה מקצר משמעותית זמן דיבוג של בדיקות נפלות. בפרויקט אחרון שלי, רק המעבר ל-#expect חתך לי בערך חצי שעה ביום של ניחושים מול XCTAssertEqual.

בנוסף, Swift Testing מתוכננת מההתחלה לעבודה עם קונקרנטיות ובידוד אקטור ב-Swift 6.2. בדיקות יכולות להיות async ולקרוא ל-await ישירות, ללא צורך ב-XCTestExpectation או callbacks. הרצה מקבילית של בדיקות מופעלת כברירת מחדל, מה שמוריד את זמן ה-CI דרמטית בפרויקטים בינוניים וגדולים.

התקנה והגדרת הסביבה ב-Xcode 26

אם אתם משתמשים ב-Xcode 16 או חדש יותר (כולל Xcode 26 הנוכחי), Swift Testing זמין מיד ללא התקנה נוספת. כשאתם יוצרים פרויקט חדש או Target של Unit Tests, Xcode שואל אותכם איזו מסגרת להשתמש, ופשוט בוחרים "Swift Testing". עבור פרויקטים קיימים, אפשר להוסיף Target חדש דרך File > New > Target > Unit Testing Bundle ולבחור Swift Testing ב-dropdown.

לפרויקטים מבוססי Swift Package Manager, השינוי ב-Package.swift מינימלי. ה-test target לא דורש dependency מפורש כי הספרייה מגיעה עם ה-toolchain:

// Package.swift
let package = Package(
    name: "MyLibrary",
    platforms: [.iOS(.v17), .macOS(.v14)],
    targets: [
        .target(name: "MyLibrary"),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary"]
        )
    ]
)

בקובץ הבדיקה עצמו, מייבאים את Testing במקום XCTest:

import Testing
@testable import MyLibrary

כתיבת הבדיקה הראשונה עם @Test ו-#expect

בדיקה ב-Swift Testing היא פשוט פונקציה גלובלית או מתודה ב-struct, מסומנת ב-@Test. אין צורך לירש ממחלקה ואין מוסכמת שמות. הנה דוגמה מינימלית של ולידציה לפונקציית חישוב מע"מ:

import Testing
@testable import Billing

@Test func calculatesVATCorrectly() {
    let total = Billing.applyVAT(to: 100, rate: 0.17)
    #expect(total == 117.0)
}

@Test("VAT on zero amount returns zero")
func vatOnZero() {
    #expect(Billing.applyVAT(to: 0, rate: 0.17) == 0)
}

שימו לב: השם הראשון של ה-@Test מקבל את שם הפונקציה כברירת מחדל. אפשר לעקוף עם מחרוזת תיאורית, וזה מומלץ, כי השם הזה מופיע ב-Test Navigator וב-CI logs. מחרוזות תיאוריות בעברית גם נתמכות במלואן.

ההבדל בין #expect ל-#require חשוב: #expect מדווח על כשלון וממשיך את הבדיקה. #require זורק שגיאה אם הביטוי שקרי ועוצר את הביצוע. השתמשו ב-#require כשהמשך הבדיקה תלוי בערך לא-nil:

@Test func parsesValidUser() throws {
    let json = #"{"id": 42, "name": "Dana"}"#.data(using: .utf8)!
    let user = try #require(try? JSONDecoder().decode(User.self, from: json))
    #expect(user.id == 42)
    #expect(user.name == "Dana")
}

בדוגמה הזו, אם הפיענוח נכשל, הבדיקה נעצרת מיד. אין טעם לבדוק שדות של אובייקט שלא קיים, ובלי #require היינו מקבלים crash על optional unwrapping.

בדיקות פרמטריות עם arguments

אחד היתרונות הבולטים של Swift Testing הוא תמיכה מובנית בבדיקות פרמטריות. במקום לכתוב 5 בדיקות נפרדות או לולאת for, מעבירים את הערכים כפרמטר ל-@Test:

@Test("Validates Israeli phone formats", arguments: [
    "050-1234567",
    "+972501234567",
    "0501234567",
    "972-50-123-4567"
])
func validIsraeliPhone(_ input: String) {
    #expect(PhoneValidator.isValid(input))
}

כל ערך במערך מופיע כבדיקה נפרדת ב-Test Navigator עם השם של הקלט המקורי. אם אחת נכשלת, השאר ממשיכות לרוץ. אפשר גם להעביר זוגות של (קלט, צפי):

@Test(arguments: [
    (0, "אפס"),
    (1, "אחד"),
    (10, "עשר"),
    (100, "מאה")
])
func hebrewNumberConversion(input: Int, expected: String) {
    #expect(HebrewNumber.spell(input) == expected)
}

למקרים שדורשים מטריצה (כל קלט A מול כל קלט B), מעבירים שני מערכים. Swift Testing יבצע את ה-cartesian product אוטומטית, ויריץ את הבדיקה לכל זוג אפשרי. בכנות, זה חסך לי מאות שורות של boilerplate בפרויקטים שמטפלים בלוקליזציה.

ארגון עם @Suite, Tags ו-Traits

כשמספר הבדיקות גדל, מארגנים אותן ב-suites. @Suite הוא struct (או class) שמכיל קבוצת בדיקות קשורות. ה-Suite יכול להחזיק state משותף שמאותחל לפני כל בדיקה, בדומה ל-setUp ב-XCTest, אבל באמצעות init רגיל של Swift:

@Suite("User Repository Tests")
struct UserRepositoryTests {
    let repo: UserRepository

    init() {
        self.repo = UserRepository(storage: InMemoryStorage())
    }

    @Test func insertsNewUser() async throws {
        try await repo.insert(User(id: 1, name: "Yael"))
        let found = try await repo.find(id: 1)
        #expect(found?.name == "Yael")
    }

    @Test func returnsNilForMissingUser() async throws {
        let found = try await repo.find(id: 999)
        #expect(found == nil)
    }
}

Swift Testing יוצר instance חדש של ה-struct לפני כל בדיקה, וזה מבטיח בידוד מלא ללא דליפת state בין בדיקות. אם אתם זקוקים ל-deinit (לניקוי משאבים), מימשו את הפרוטוקול Sendable במידת הצורך והוסיפו deinit כרגיל.

Tags מאפשרים לסמן בדיקות לפי קטגוריה ולהריץ רק חלק מהן. הגדירו Tag ב-extension:

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

@Test(.tags(.slow, .network))
func fetchesUserProfileFromAPI() async throws {
    let profile = try await APIClient.fetchProfile(id: 1)
    #expect(profile.id == 1)
}

בשורת הפקודה אפשר להריץ swift test --filter tag:slow או להחריג --skip tag:network. זה שימושי במיוחד ל-CI: בדיקות מהירות בכל PR, בדיקות איטיות פעם בלילה.

בדיקות אסינכרוניות ו-Swift Concurrency

Swift Testing נבנתה מהיום הראשון מסביב ל-async/await. כל בדיקה יכולה להיות async ולקרוא ל-await ישירות, ללא XCTestExpectation או DispatchSemaphore. הנה דוגמה לבדיקה של פונקציה שמושכת נתונים מ-Foundation Models ב-iOS 26:

@Test func generatesShortSummary() async throws {
    let model = LanguageModel.system
    let response = try await model.respond(
        to: "Summarize in 5 words: \(longArticle)"
    )
    #expect(response.text.split(separator: " ").count <= 5)
}

לבדיקת זריקת שגיאות יש מאקרו ייעודי, #expect(throws:):

@Test func rejectsInvalidEmail() {
    #expect(throws: ValidationError.invalidEmail) {
        try EmailValidator.validate("not-an-email")
    }
}

@Test func rejectsAnyError() {
    #expect(throws: (any Error).self) {
        try riskyOperation()
    }
}

לבדיקות עם timeout (למשל network calls שעלולים להיתקע), עוטפים את הקריאה ב-withTimeLimit trait:

@Test(.timeLimit(.minutes(1)))
func uploadCompletesWithinMinute() async throws {
    try await Uploader.send(largeFile)
}

ה-trait הזה מבטיח שהבדיקה תיכשל אם היא רצה יותר מדקה, גם אם הקוד עצמו תקוע. אישית, נתקלתי בזה כשבדיקת upload התקשתה לתפוס connection timeout, וזה החזיק לי build של 45 דקות לחינם.

לבדיקה של MainActor-isolated code, אפשר לסמן את הבדיקה ב-@MainActor:

@Test @MainActor
func viewModelUpdatesUIState() async {
    let vm = LoginViewModel()
    await vm.submit(email: "[email protected]", password: "123")
    #expect(vm.isLoading == false)
    #expect(vm.errorMessage == nil)
}

הקומפיילר יוודא שכל הגישות לתכונות מבודדות-MainActor חוקיות, ללא צורך ב-await מפורש לכל קריאה. זה משתלב היטב עם ViewModels של SwiftUI שמסומנים ב-@Observable או ב-@MainActor.

מיגרציה מ-XCTest ל-Swift Testing

אין צורך להמיר את כל הבדיקות ביום אחד. Apple תכננה את Swift Testing לעבוד זה לצד זה עם XCTest באותו test target. שתי המסגרות רצות במקביל, ה-Test Navigator מציג את שתיהן יחד, וכל אחת משתמשת ב-runner שלה. המדריך הרשמי של Apple למיגרציה מ-XCTest ממליץ על אסטרטגיה הדרגתית: קבצים חדשים נכתבים ב-Swift Testing, קבצים קיימים מומרים לפי הצורך.

טבלת המרה מהירה של ה-API הנפוצים:

XCTestSwift Testingהערות
XCTestCase subclassstruct או פונקציה גלובליתאין צורך בירושה
func testFoo()@Test func foo()אין מוסכמת שמות
XCTAssertEqual(a, b)#expect(a == b)השוואה רגילה של Swift
XCTAssertNil(x)#expect(x == nil)
XCTUnwrap(x)try #require(x)נכשלת ועוצרת
XCTAssertThrowsError#expect(throws:)תומך בסוג שגיאה ספציפי
setUp / tearDowninit / deinitפר-בדיקה אוטומטית
XCTSkip.disabled("reason") traitאו withKnownIssue
XCTExpectationasync/await ישירבדרך כלל לא נחוץ

הצעד הראשון הטוב הוא להמיר קובץ אחד פשוט, להריץ את הסוויטה ולוודא שהכל עובד. אחר כך התקדמו לפי קטגוריות: networking, persistence, business logic. שמרו את ה-UI Tests וה-Performance Tests ב-XCTest, כי Swift Testing עדיין לא תומך בהם נכון ל-2026, וזה לא צפוי להשתנות בקרוב.

שגיאה נפוצה במיגרציה: ניסיון להעביר ירושה מ-XCTestCase ל-struct של @Suite ולשמור על override של setUp. במקום זאת, השתמשו ב-init רגיל. אם ההכנה דורשת פעולות אסינכרוניות, הגדירו את ה-init כ-async throws. Swift Testing תומך בזה מאז גרסה 0.10:

@Suite("Database Tests")
struct DatabaseTests {
    let db: Database

    init() async throws {
        self.db = try await Database.openInMemory()
        try await db.migrate()
    }
}

טיפ חשוב לדיבוג: כאשר #expect נכשל עם ביטוי מורכב, ה-output כולל את הערך של כל תת-ביטוי. למשל #expect(user.profile.age >= 18) ידפיס את user.profile.age במפורש, מה שחוסך הרבה הוספת print. לפרטי הרחבת המאקרו ראו את תיעוד Swift Testing הרשמי של Apple.

שאלות נפוצות

האם Swift Testing מחליף את XCTest לגמרי?

לא לחלוטין. Swift Testing מחליף את XCTest עבור unit tests, אבל UI Tests, Performance Tests ובדיקות שדורשות XCUIApplication עדיין מחייבות את XCTest. שתי המסגרות יכולות לחיות באותו פרויקט ולרוץ במקביל באותה סוויטה.

מהן דרישות הגרסה ל-Swift Testing?

נדרש Xcode 16 ומעלה ו-Swift 6 toolchain. Swift Testing פועל על iOS 16+, macOS 13+, watchOS 9+, tvOS 16+, visionOS 1+, וגם על Linux ו-Windows דרך Swift Package Manager. הספרייה עצמה היא open-source ולא קשורה ל-SDK של Apple.

איך מריצים בדיקה ספציפית משורת הפקודה?

השתמשו ב-swift test --filter NameOfTest כדי להריץ בדיקה אחת, או --filter SuiteName/testName לבדיקה בתוך Suite. אפשר גם לסנן לפי tag עם --filter tag:integration או להחריג עם --skip tag:slow.

האם בדיקות ב-Swift Testing באמת רצות במקביל?

כן, כברירת מחדל. כל בדיקה רצה ב-Task נפרד ויכולה לרוץ במקביל לאחרות. אם בדיקה אינה בטוחה ב-concurrency (למשל ניגשת ל-database משותף), הוסיפו את ה-trait .serialized ל-@Suite או ל-@Test ספציפי כדי לאלץ ריצה סדרתית.

איך כותבים מבחני ביצועים ב-Swift Testing?

נכון ל-2026 אין עדיין מקבילה רשמית ל-measure block של XCTest. למבחני ביצועים השאירו את הבדיקות ב-XCTestCase. Apple רמזה בכמה sessions שתמיכה ב-performance metrics מתוכננת לגרסה עתידית של Swift Testing.

Editorial Team
אודות הכותב Editorial Team

Our team of expert writers and editors.