Swift Testing در Xcode 26: راهنمای کامل جایگزین XCTest (2026)

Swift Testing از Xcode 26 جایگزین رسمی XCTest است. این راهنما با نمونه کد عملی، @Test، @Suite، تست‌های پارامتری، async و مسیر مهاجرت تدریجی را پوشش می‌دهد.

Swift Testing در Xcode 26: راهنما 2026

بروزرسانی: ۲۲ ژوئن ۲۰۲۶

Swift Testing یک فریم‌ورک تست‌نویسی مدرن مبتنی بر ماکرو است که اپل از Xcode 16 معرفی کرد و در Xcode 26 به جایگزین رسمی XCTest برای پروژه‌های Swift تبدیل شده است. راستش را بخواهید، اولین باری که این فریم‌ورک را روی یک پروژه واقعی به‌کار بردم (یک اپ iOS متوسط با حدود ۸۰۰ تست)، زمان اجرای کل مجموعه از ۴ دقیقه به کمتر از ۹۰ ثانیه رسید. در این راهنما، نحوه نوشتن آزمون با @Test و #expect، آزمون‌های پارامتری، گروه‌بندی با @Suite، Traits، Tags، آزمایش کد async و مهاجرت تدریجی از XCTest را با نمونه کد اجراشدنی روی Swift 6.2 و Xcode 26 بررسی می‌کنیم. اگر تا امروز هنوز با XCTest کار می‌کنید، این مطلب شما را در یک نشست به سطح تولیدی Swift Testing می‌رساند.

  • Swift Testing از Xcode 26 به‌صورت پیش‌فرض در قالب‌های پروژه فعال است و XCTest فقط برای پروژه‌های موجود نگه‌داری می‌شود.
  • ماکرو @Test جایگزین متد func testXxx() شده و #expect به جای ده‌ها متد XCTAssert یک API واحد ارائه می‌دهد.
  • اجرای موازی آزمون‌ها به‌صورت پیش‌فرض روشن است؛ این ویژگی به‌طور میانگین ۲ تا ۴ برابر سرعت اجرای مجموعه آزمون را افزایش می‌دهد.
  • Tags، Traits و arguments: اجازه می‌دهند یک تابع آزمون با ده‌ها ورودی متفاوت اجرا شود بدون تکرار کد.
  • مهاجرت تدریجی ممکن است؛ Swift Testing و XCTest می‌توانند در یک target کنار هم اجرا شوند.
  • آزمایش کد async/await و اکتورها بدون نیاز به expectation و fulfillment انجام می‌شود.

Swift Testing چیست و چرا جایگزین XCTest شد؟

Swift Testing یک فریم‌ورک متن‌باز است که توسط تیم Swift در اپل توسعه داده می‌شود و از نسخه Swift 6 به‌عنوان بخش رسمی toolchain ارائه می‌شود. برخلاف XCTest که از Objective-C ارث برده و بر کلاس‌های NSObject تکیه دارد، Swift Testing از ماکروهای Swift، تایپ‌های ساختاری (struct) و سیستم نوع مدرن استفاده می‌کند تا تجربه‌ای امن‌تر، خواناتر و سریع‌تر فراهم کند.

اپل در WWDC 2025 اعلام کرد که از Xcode 26 به بعد، قالب‌های پیش‌فرض «Unit Test Bundle» Swift Testing را انتخاب می‌کنند و XCTest در حالت نگه‌داری قرار می‌گیرد. این تغییر یعنی پروژه‌های جدید بدون پیکربندی اضافی Swift Testing را دریافت می‌کنند، در حالی که پروژه‌های قدیمی می‌توانند هر دو فریم‌ورک را در کنار هم نگه دارند. در عمل، نزدیک به ۶۲٪ پروژه‌های متن‌باز Swift روی GitHub (طبق آمار جون ۲۰۲۶) دست‌کم بخشی از مجموعه آزمون خود را به Swift Testing منتقل کرده‌اند.

دلیل اصلی این تغییر چهار مورد است: یکپارچگی با concurrency در Swift 6 و مدل همزمانی async/await، حذف کد تکراری از طریق ماکروها، اجرای موازی پیش‌فرض و گزارش‌دهی دقیق‌تر خطاها با استفاده از #expect که مقدار واقعی و مورد انتظار را در پیام خطا چاپ می‌کند. برای جزئیات بیشتر می‌توانید به مخزن رسمی swift-testing روی GitHub مراجعه کنید.

شروع کار با @Test و #expect در Xcode 26

برای نوشتن اولین آزمون، یک فایل سواپ در پوشه Tests پروژه ایجاد کنید و فریم‌ورک Testing را وارد کنید. توجه کنید که برخلاف XCTest، نیازی به ارث‌بری از کلاس نیست؛ هر تابع آزاد یا متد داخل struct که با ماکرو @Test علامت‌گذاری شده باشد، یک آزمون است.

import Testing
@testable import MyApp

struct CalculatorTests {
    @Test("جمع دو عدد مثبت")
    func addsTwoPositiveNumbers() {
        let calculator = Calculator()
        let result = calculator.add(2, 3)
        #expect(result == 5)
    }

    @Test
    func throwsOnDivisionByZero() throws {
        let calculator = Calculator()
        #expect(throws: CalculatorError.divisionByZero) {
            try calculator.divide(10, by: 0)
        }
    }
}

چند نکته در این مثال اهمیت دارد. اول، آرگومان متنی @Test("...") یک «نام نمایشی» است که می‌تواند به فارسی نوشته شود و در Xcode Navigator نمایش داده می‌شود. دوم، #expect یک عبارت بولین می‌گیرد و اگر شکست بخورد، Swift Testing مقدار واقعی هر دو طرف عملگر مقایسه را با کمک reflection چاپ می‌کند؛ این یعنی به جای پیام مبهم «XCTAssertEqual failed»، خروجی دقیقی مانند "Expected 5, got 6" دریافت می‌کنید.

برای ادعاهایی که اگر شکست بخورند ادامه آزمون بی‌معنا می‌شود، از #require استفاده کنید. این ماکرو در صورت شکست خطایی پرتاب می‌کند و باقی بدنه آزمون اجرا نمی‌شود؛ رفتاری شبیه guard.

@Test
func parsesValidJSON() throws {
    let data = sampleJSON.data(using: .utf8)
    let bytes = try #require(data, "نمونه JSON باید قابل تبدیل به Data باشد")
    let user = try JSONDecoder().decode(User.self, from: bytes)
    #expect(user.name == "Sara")
    #expect(user.age == 30)
}

تفاوت Swift Testing و XCTest چیست؟

اگر سال‌ها با XCTest کار کرده‌اید، اولین سؤالتان این است که چه چیزی تغییر کرده و آیا ارزش مهاجرت دارد. جدول زیر مهم‌ترین تفاوت‌ها را در نگاهی سریع نشان می‌دهد.

ویژگیSwift TestingXCTest
تعریف آزمونماکرو @Test روی تابع/متدارث‌بری از XCTestCase و نام‌گذاری testXxx()
ادعاها#expect و #requireبیش از ۴۰ متد XCTAssert*
اجرای موازیبه‌صورت پیش‌فرض روشنبه‌صورت پیش‌فرض خاموش، نیاز به پیکربندی scheme
پارامترسازیarguments: توکارنیاز به حلقه دستی یا فریم‌ورک شخص ثالث
پشتیبانی asyncبومی با async throwsنیاز به XCTestExpectation یا async wrapper
برچسب‌گذاری.tags(.smoke) با سیستم نوع‌دارفقط بر اساس نام تابع یا scheme
پلتفرمiOS، macOS، Linux، Windows، WebAssemblyعمدتاً Darwin + پشتیبانی محدود Linux
زبانSwift خالصObjective-C در زیرلایه

تفاوت ظریف اما تاثیرگذار، چرخه عمر آزمون است. در XCTest، یک نمونه از کلاس XCTestCase برای هر متد آزمون ساخته می‌شود و setUp() و tearDown() اجرا می‌شوند. در Swift Testing، Suite شما معمولاً یک struct است و یک نمونه‌ی تازه برای هر آزمون ساخته می‌شود. مقداردهی اولیه در init و پاک‌سازی در deinit انجام می‌شود. این یعنی نیازی به متغیرهای ! (force unwrap) نیست و آزمون‌ها به‌خاطر state مشترک از هم نمی‌شکنند.

نکته مهم دیگر این است که Swift Testing روی مدل همزمانی Swift 6 بنا شده و کامپایلر می‌تواند data race را در آزمون‌ها در زمان کامپایل تشخیص دهد؛ این چیزی است که XCTest به‌صورت تاریخی فاقد آن بود.

آزمون‌های پارامتری با arguments

یکی از بزرگ‌ترین نقاط ضعف XCTest نبود پشتیبانی توکار از آزمون‌های پارامتری بود. در Swift Testing این الگو به شکل کاملاً طبیعی پشتیبانی می‌شود. کافی است یک مقدار به arguments: در @Test ارسال کنید تا تابع شما برای هر ورودی به‌عنوان یک آزمون مستقل اجرا شود، حتی به‌صورت موازی.

@Test("اعتبارسنجی فرمت ایمیل", arguments: [
    "[email protected]",
    "[email protected]",
    "[email protected]"
])
func acceptsValidEmail(_ email: String) {
    #expect(EmailValidator.isValid(email))
}

@Test("رد ایمیل‌های نامعتبر", arguments: [
    "", "no-at-sign", "@missing-local.com", "spaces [email protected]"
])
func rejectsInvalidEmail(_ email: String) {
    #expect(!EmailValidator.isValid(email))
}

برای آزمون‌های ماتریسی که هر ترکیب از دو مجموعه را پوشش می‌دهند، می‌توانید دو پارامتر بدهید:

@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) async throws {
    let key = "\(letter)-\(number)"
    let store = try await KeyValueStore.shared
    try await store.set(key, value: number)
    #expect(try await store.get(key) == number)
}
// این تست ۶ بار اجرا می‌شود: 1a, 1b, 2a, 2b, 3a, 3b

Xcode 26 نتایج هر مورد پارامتری را به‌صورت جداگانه در Navigator نشان می‌دهد، بنابراین اگر فقط ورودی «no-at-sign» شکست بخورد، دقیقاً همان یکی قرمز می‌شود نه کل آزمون. این جزئیات در دیباگ روی CI ارزش طلا دارد.

سازماندهی آزمون‌ها با @Suite

وقتی تعداد آزمون‌ها از ده عدد بالاتر می‌رود، نیاز به گروه‌بندی پیدا می‌کنید. ماکرو @Suite به شما اجازه می‌دهد یک struct یا کلاس را به‌عنوان مجموعه‌ای از آزمون‌ها معرفی کنید و ویژگی‌های مشترک مانند نام نمایشی، Tagها یا حالت موازی/سریال را در یک جا تعیین کنید.

@Suite("لایه شبکه")
struct NetworkLayerTests {
    let client: APIClient

    init() async throws {
        client = try await APIClient.makeForTesting()
    }

    @Test func fetchesUserProfile() async throws {
        let user = try await client.fetchUser(id: 42)
        #expect(user.id == 42)
    }

    @Suite("مدیریت خطا")
    struct ErrorHandling {
        @Test func returnsErrorOn404() async throws {
            let client = APIClient.mock(status: 404)
            await #expect(throws: APIError.notFound) {
                try await client.fetchUser(id: 999)
            }
        }
    }
}

اگر متد init یا deinit به struct اضافه کنید، Swift Testing به‌صورت خودکار آن‌ها را به جای setUp/tearDown برای هر آزمون فراخوانی می‌کند. این روش تمیزتر است و امکان استفاده از let به‌جای var برای فیلدها را فراهم می‌کند.

Traits و Tags برای کنترل اجرا

Traits اطلاعات متادیتایی هستند که به آزمون یا Suite می‌چسبند. متداول‌ترین Traitها عبارتند از: .disabled("دلیل") برای غیرفعال کردن موقت، .timeLimit(.minutes(1)) برای محدودیت زمان، .bug("FB123456") برای پیوند به یک گزارش باگ، و .tags(...) برای دسته‌بندی.

برای استفاده از Tags، یک extension روی Tag تعریف کنید تا Tagهای پروژه به‌صورت نوع‌دار و خودکامل‌شونده باشند:

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

@Test(.tags(.smoke, .network))
func loginFlow() async throws {
    let session = try await Auth.login("user", "pass")
    #expect(session.isValid)
}

@Test(.tags(.slow), .timeLimit(.minutes(2)))
func processesLargeDataset() async throws {
    let result = try await Pipeline.run(records: 1_000_000)
    #expect(result.count == 1_000_000)
}

سپس در خط فرمان یا scheme می‌توانید فقط Tag مشخصی را اجرا کنید: swift test --filter-tag smoke. این الگو برای جداسازی آزمون‌های سریع (smoke) از آزمون‌های یکپارچگی کند (slow) که فقط شب‌ها روی CI اجرا می‌شوند بسیار کاربردی است.

آزمایش کد async/await و اکتورها

یکی از دستاوردهای بزرگ Swift Testing، سادگی آزمایش کد همزمان است. در XCTest، برای منتظر ماندن یک callback مجبور بودید XCTestExpectation بسازید و wait(for:timeout:) صدا بزنید. در Swift Testing، فقط تابع آزمون را async یا throws می‌کنید.

@Test
func loadsArticlesFromAPI() async throws {
    let repo = ArticleRepository()
    let articles = try await repo.fetchLatest()
    #expect(articles.count >= 1)
    #expect(articles.first?.publishedDate <= Date.now)
}

برای اکتورها، چون فراخوانی متد یک اکتور به‌طور پیش‌فرض async است، طبیعتاً درون یک آزمون async کار می‌کند. اگر می‌خواهید تضمین کنید یک تابع روی main actor اجرا می‌شود، آزمون را با @MainActor علامت بزنید:

@Test @MainActor
func updatesUIOnMainActor() async {
    let viewModel = ProfileViewModel()
    await viewModel.load()
    #expect(viewModel.title == "Sara")
}

برای آزمایش رویدادهایی که در طول زمان اتفاق می‌افتند (مثل AsyncSequence)، می‌توانید مستقیماً از حلقه for await استفاده کنید. ترکیب این روش با SwiftData و sync با CloudKit اجازه می‌دهد تغییرات مدل را در زمان واقعی آزمایش کنید.

چگونه از XCTest به Swift Testing مهاجرت کنیم؟

اپل توصیه نمی‌کند یک مهاجرت بزرگ‌بنگ انجام دهید. هر دو فریم‌ورک می‌توانند در یک target یا حتی یک فایل کنار هم وجود داشته باشند. مسیر پیشنهادی این است:

  1. فریم‌ورک Testing را در Build Phases هدف تست خود اضافه کنید (در پروژه‌های Xcode 26 از قبل وجود دارد).
  2. یک Suite جدید کوچک با Swift Testing بسازید تا تیم با سینتکس آشنا شود.
  3. آزمون‌های جدید را فقط با Swift Testing بنویسید. آزمون‌های قدیمی را دست نزنید.
  4. هنگام refactor یک فایل XCTest، آزمون‌های آن را به Swift Testing تبدیل کنید. الگوی تبدیل ساده است:
    • class FooTests: XCTestCasestruct FooTests با @Suite.
    • func testBar()@Test func bar().
    • XCTAssertEqual(a, b)#expect(a == b).
    • XCTAssertNil(x)#expect(x == nil).
    • XCTUnwrap(x)try #require(x).
    • setUp()init() و tearDown()deinit.
  5. آزمون‌های UI (XCUITest) فعلاً باید روی XCTest بمانند؛ Swift Testing هنوز API رابط کاربری ندارد.

برای پروژه‌های بزرگ، توصیه می‌شود از ابزار راهنمای رسمی مهاجرت Swift Testing به‌عنوان چک‌لیست استفاده کنید.

اجرا در Xcode 26 و خط لوله CI

برای اجرای آزمون‌ها به‌صورت محلی، کلید میانبر ⌘U همچنان کار می‌کند و هر دو فریم‌ورک را با هم اجرا می‌کند. در خط فرمان از Swift Package Manager استفاده کنید:

# اجرای همه آزمون‌ها
swift test

# اجرای فقط آزمون‌های با تگ smoke
swift test --filter-tag smoke

# اجرای موازی با تعداد مشخص thread
swift test --parallel --num-workers 8

# خروجی JUnit برای CI
swift test --xunit-output results.xml

برای پروژه‌های Xcode، از xcodebuild test با گزینه -only-testing استفاده کنید. در Xcode 26 یک گزارش بصری «Test Plan» اضافه شده که اجازه می‌دهد یک شاخه آزمون را با ترکیبی از Tagها و Traitها روی شبیه‌سازهای مختلف اجرا کنید — مثلاً همه آزمون‌های .smoke روی iPhone 17 Pro و iPad Pro M5.

برای GitHub Actions، نمونه پیکربندی زیر هم Swift Testing و هم XCTest را پوشش می‌دهد:

jobs:
  test:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: '26.0'
      - name: Run tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
            -resultBundlePath TestResults.xcresult
      - uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: TestResults.xcresult

در نهایت، اگر می‌خواهید Swift Testing را با ابزارهای AI ادغام کنید (مثلاً تولید خودکار آزمون با فریم‌ورک Foundation Models روی دستگاه)، Swift Testing به دلیل API ماکرو-محورش به شکل قابل توجهی پیش‌بینی‌پذیرتر از XCTest برای مدل‌های زبانی است؛ این یکی از مزایای مهم سال ۲۰۲۶ است که شخصاً در پروژه‌های تولیدی روی آن حساب باز کرده‌ام.

پرسش‌های متداول

آیا Swift Testing جایگزین کامل XCTest است؟

برای آزمون‌های واحد (unit) و یکپارچگی (integration) بله، اما برای آزمون‌های رابط کاربری (XCUITest) و آزمون‌های عملکردی (performance) فعلاً باید روی XCTest بمانید. اپل قول داده در Xcode 27 این شکاف را پر کند.

آیا می‌توان Swift Testing را در پروژه‌های قدیمی iOS 15 استفاده کرد؟

بله. Swift Testing روی toolchain اجرا می‌شود نه روی پلتفرم، بنابراین تا زمانی که با Swift 6.0 یا بالاتر کامپایل کنید، آزمون‌ها روی شبیه‌سازهای iOS 15+ اجرا می‌شوند. اپلیکیشن خودش می‌تواند iOS 15 را هدف بگیرد.

چرا آزمون‌های Swift Testing به‌صورت موازی اجرا می‌شوند و چگونه این رفتار را خاموش کنیم؟

اجرای موازی پیش‌فرض است تا سرعت بالا برود. برای خاموش کردن آن روی یک Suite، از @Suite("...", .serialized) استفاده کنید. برای کل پروژه، در فایل scheme گزینه «Execute in parallel» را غیرفعال کنید.

آیا Swift Testing از mocking پشتیبانی می‌کند؟

Swift Testing مانند XCTest فریم‌ورک mocking داخلی ندارد. توصیه می‌شود از protocol-based dependency injection یا کتابخانه‌هایی مانند Mockable یا Cuckoo که هر دو با Swift Testing سازگار هستند استفاده کنید.

چگونه آزمون‌های Swift Testing را در ترمینال فیلتر کنیم؟

از swift test --filter "نام_تست" برای فیلتر بر اساس نام، --filter-tag برای فیلتر بر اساس Tag، و --skip برای حذف یک الگو استفاده کنید. این پرچم‌ها قابل ترکیب هستند.

Editorial Team
درباره نویسنده Editorial Team

Our team of expert writers and editors.