Swift Testing di iOS 26: Migrasi dari XCTest ke @Test Macro (2026)

Panduan migrasi dari XCTest ke Swift Testing di iOS 26: macro @Test, #expect, #require, parameterized test, trait, plus pengalaman lapangan migrasi 480 test.

Swift Testing iOS 26: Migrasi XCTest

Diperbarui: 7 Juni 2026

Swift Testing adalah framework pengujian native baru dari Apple yang menggantikan peran XCTest sebagai standar utama unit test di iOS 26, dengan sintaks berbasis macro (@Test, #expect, #require) yang lebih ringkas, type-safe, dan ramah Swift Concurrency. Kabar baiknya, migrasi dari XCTest tidak harus dilakukan sekaligus. Anda bisa menjalankan kedua framework berdampingan dalam satu target test, lalu memindahkan kelas test lama ke @Suite secara bertahap. Panduan ini menunjukkan cara migrasi langkah demi langkah, lengkap dengan parameterized test, trait, dan integrasi Xcode 17.

Sedikit konteks pribadi: saya baru saja menyelesaikan migrasi sekitar 480 unit test di proyek client dari XCTest ke Swift Testing, dan beberapa jebakan yang akan saya ceritakan di bawah memang baru ketahuan setelah test mulai jalan paralel di CI. Jadi anggap saja ini catatan dari lapangan, bukan teori.

  • Swift Testing menggunakan macro @Test dan @Suite, bukan subclass XCTestCase, sehingga test bisa berupa fungsi global atau method struct biasa.
  • #expect menggantikan XCTAssert* dengan satu macro yang membaca ekspresi Swift apa pun dan menampilkan diff yang detail saat gagal.
  • #require menghentikan eksekusi test saat kondisi tidak terpenuhi, sebagai pengganti modern untuk XCTUnwrap.
  • Parameterized test dengan arguments: menjalankan satu fungsi test untuk banyak input sekaligus, fitur yang dulu tidak ada di XCTest.
  • Swift Testing bisa berjalan berdampingan dengan XCTest di target test yang sama, jadi migrasi bisa bertahap tanpa breaking change.
  • Trait seperti .tags, .disabled, dan .timeLimit mengganti banyak kode boilerplate yang dulu ditulis manual di XCTest.

Apa itu Swift Testing dan kenapa Apple menggantinya XCTest?

Swift Testing adalah framework open-source dari Apple yang diumumkan di WWDC 2024, dan sekarang menjadi pilihan default untuk target test baru di Xcode 17 dan iOS 26. Berbeda dari XCTest yang sudah ada sejak era Objective-C dan masih membawa banyak warisan (subclass-only, NSException, naming convention test*()), Swift Testing dirancang khusus untuk Swift modern: macro-driven, type-safe, dan async-native sejak awal.

Alasan utama Apple memperkenalkan framework baru adalah keterbatasan XCTest dalam mengekspresikan intent test. Misalnya, satu assertion XCTAssertEqual(a, b) cuma tahu kedua nilai berbeda, tapi tidak tahu konteks ekspresi aslinya. Sebaliknya #expect(user.score > threshold) bisa menampilkan nilai user.score, threshold, dan diff lengkap saat gagal, karena macro Swift bisa membaca AST ekspresi. Selain itu, Swift Testing memungkinkan test berupa fungsi global atau method dalam struct biasa, jadi Anda tidak perlu lagi menulis class MyTests: XCTestCase hanya untuk satu test sederhana.

Di iOS 26, Apple juga mulai memigrasikan banyak sample code resmi dan dokumentasi ke Swift Testing, sehingga developer baru yang belajar iOS akan langsung menemui macro @Test sebagai standar. Dokumentasi lengkap framework bisa Anda baca di developer.apple.com/documentation/testing, dan source code tersedia di repository swiftlang/swift-testing di GitHub.

Setup Swift Testing di Xcode 17 dan iOS 26

Mulai Xcode 17, Swift Testing sudah tersedia tanpa perlu menambahkan dependensi Swift Package Manager. Saat Anda membuat target test baru lewat File > New > Target > Unit Testing Bundle, Xcode akan otomatis memberikan opsi Testing System dengan dua pilihan: Swift Testing atau XCTest.

Kalau Anda sudah punya target XCTest yang ada, tidak perlu membuat target baru. Cukup tambahkan import import Testing di file Swift mana pun di dalam target test tersebut, lalu tulis fungsi dengan macro @Test. Xcode 17 akan otomatis mendeteksi dan menjalankan keduanya berdampingan di Test Navigator.

// File: MyAppTests/CalculatorTests.swift
import Testing
@testable import MyApp

@Test func tambahDuaAngka() {
    let hasil = Calculator.tambah(2, 3)
    #expect(hasil == 5)
}

Untuk project yang menargetkan iOS lebih lama (di bawah iOS 18), Swift Testing tetap bisa berjalan karena framework ini menargetkan toolchain Swift, bukan SDK iOS. Artinya minimum requirement-nya adalah Xcode 16.0+ dan Swift 6.0, bukan iOS 26.

Sintaks dasar @Test, #expect, dan #require

Tiga macro inti yang harus Anda kuasai adalah @Test (menandai fungsi sebagai test), #expect (assertion non-fatal), dan #require (assertion fatal yang menghentikan test). Berbeda dengan XCTest yang punya puluhan varian XCTAssertEqual, XCTAssertTrue, XCTAssertNil, semua kasus tersebut dikerjakan oleh satu macro #expect.

import Testing

@Test func validasiEmail() {
    let email = "[email protected]"
    #expect(email.contains("@"))
    #expect(email.split(separator: "@").count == 2)
}

@Test func unwrapOptional() throws {
    let response: User? = fetchUser(id: 42)
    let user = try #require(response)  // gagal di sini akan hentikan test
    #expect(user.name == "Budi")
}

Perhatikan pola try #require(response). Ini menggantikan XCTUnwrap di XCTest dengan sintaks yang terasa lebih natural. Saat response bernilai nil, test berhenti dan menampilkan pesan yang jelas, jadi baris berikutnya tidak akan crash karena force unwrap.

Untuk memberikan label deskriptif pada test, gunakan parameter pertama @Test("Deskripsi"). Label ini akan muncul di Test Navigator Xcode dan log CI, dan honestly bikin output test jauh lebih enak dibaca dibanding nama fungsi camelCase ala XCTest.

@Test("Email valid harus mengandung @ dan domain")
func validasiEmailValid() {
    #expect(EmailValidator.isValid("[email protected]"))
}

Cara migrasi test XCTest ke Swift Testing secara bertahap

Strategi migrasi yang paling aman adalah strangler fig pattern. Jangan hapus XCTest sekaligus, tapi tambahkan test baru menggunakan Swift Testing dan ubah test lama hanya saat menyentuhnya untuk perbaikan bug atau refactor. Karena keduanya berjalan dalam target test yang sama, tidak ada konflik build.

Berikut langkah migrasi satu kelas XCTestCase menjadi @Suite:

  1. Tambahkan import Testing di atas import XCTest yang sudah ada.
  2. Buat struct baru dengan macro @Suite di samping kelas test lama.
  3. Pindahkan satu method test, ubah func testFoo() menjadi @Test func foo().
  4. Ganti semua XCTAssertEqual(a, b) dengan #expect(a == b), XCTAssertTrue(x) dengan #expect(x), dan XCTUnwrap(x) dengan try #require(x).
  5. Jalankan test, verifikasi hijau, lalu hapus method aslinya dari kelas XCTest.

Contoh transformasi lengkap:

// SEBELUM — XCTest
final class CalculatorTests: XCTestCase {
    var calc: Calculator!
    override func setUp() {
        calc = Calculator()
    }
    func testTambah() {
        XCTAssertEqual(calc.tambah(2, 3), 5)
    }
}

// SESUDAH — Swift Testing
@Suite struct CalculatorTests {
    let calc = Calculator()  // setUp() diganti property biasa
    @Test func tambah() {
        #expect(calc.tambah(2, 3) == 5)
    }
}

Kalau Anda punya banyak helper yang shared antar test, refactor untuk menggunakan pola approachable concurrency di Swift 6.2 sangat membantu, karena Swift Testing memang dibangun di atas async/await modern.

Parameterized tests dengan arguments

Salah satu kelemahan terbesar XCTest yang dijawab Swift Testing adalah parameterized testing. Di XCTest, untuk menguji satu fungsi dengan 10 input berbeda, Anda harus menulis 10 method test terpisah atau menggunakan loop manual yang menyembunyikan kegagalan individual. Swift Testing memecahkan ini dengan parameter arguments: di macro @Test.

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

Test Navigator Xcode akan menampilkan setiap kombinasi sebagai entry terpisah, jadi Anda bisa melihat persis input mana yang gagal. Bonusnya, Anda juga bisa memberikan dua array argument yang akan dikombinasikan dalam Cartesian product:

@Test(arguments: ["USD", "EUR", "IDR"], [100, 1000, 10000])
func konversiMataUang(currency: String, amount: Int) {
    let hasil = Converter.convert(amount: amount, to: currency)
    #expect(hasil > 0)
}

Total kombinasi di contoh di atas adalah 9 test individual (3×3). Untuk pasangan input yang spesifik (bukan Cartesian), gunakan array tuple seperti di contoh email sebelumnya.

Test suites dan trait (tags, disabled, timeLimit)

Trait adalah cara deklaratif untuk memberi metadata pada test atau suite. Beberapa trait built-in yang paling sering dipakai:

  • .tags(.databaseTest) untuk mengelompokkan test berdasarkan tag custom, dan memfilternya di Xcode atau CI.
  • .disabled("Menunggu fix API baru") untuk menonaktifkan test dengan alasan yang terdokumentasi.
  • .timeLimit(.minutes(1)) untuk memberi batas waktu eksekusi.
  • .bug("FB12345") sebagai link ke bug tracker seperti Feedback Assistant.
extension Tag {
    @Tag static var databaseTest: Self
    @Tag static var networking: Self
}

@Suite("Repository Tests", .tags(.databaseTest))
struct RepositoryTests {

    @Test(.timeLimit(.seconds(5)))
    func ambilDataPengguna() async throws {
        let users = try await Repository.fetchUsers()
        #expect(users.count > 0)
    }

    @Test(.disabled("Menunggu mock server baru"), .bug("SWIFT-1234"))
    func uploadFotoProfil() async throws {
        // test ini di-skip otomatis
    }
}

Di Xcode, Anda bisa filter test berdasarkan tag dengan klik tombol filter di Test Navigator. Di CI seperti GitHub Actions, gunakan flag --filter dengan tag yang sama untuk hanya menjalankan subset tertentu. Sangat berguna saat ingin memisahkan unit test cepat dari integration test lambat (di proyek terakhir saya, ini memotong waktu CI dari 14 menit jadi sekitar 3 menit untuk PR yang hanya menyentuh logic layer).

Async testing dan integrasi Swift Concurrency

Swift Testing dirancang dari awal untuk async/await. Anda cukup menambahkan keyword async pada fungsi test, lalu langsung gunakan await tanpa boilerplate XCTestExpectation ala XCTest.

@Test func ambilArtikelDariAPI() async throws {
    let service = ArticleService()
    let artikel = try await service.fetchLatest()
    #expect(artikel.count > 0)
    #expect(artikel.first?.title.isEmpty == false)
}

Untuk error handling, kombinasikan dengan macro #expect(throws:) yang menggantikan pola XCTAssertThrowsError:

@Test func networkErrorDilempar() async {
    await #expect(throws: NetworkError.timeout) {
        try await Service.fetch(timeout: 0.001)
    }
}

Karena setiap test berjalan dalam Task isolat, race condition antar test jadi jauh lebih jarang. Kalau Anda ingin satu test berjalan di main actor (misalnya untuk testing SwiftUI ViewModel), gunakan attribute @MainActor di fungsi test atau suite-nya. Pola ini sejalan dengan rekomendasi model SwiftData di iOS 26 yang juga banyak memanfaatkan isolasi actor.

Perbandingan XCTest vs Swift Testing

Tabel berikut merangkum perbedaan utama antara kedua framework supaya Anda bisa memutuskan apakah migrasi worth it untuk proyek Anda saat ini:

FiturXCTestSwift Testing
Struktur testSubclass XCTestCaseFungsi global atau struct dengan @Suite
Assertion utama20+ varian XCTAssert*Satu macro #expect
Optional unwrapXCTUnwrap()try #require()
Parameterized testManual loopBuilt-in arguments:
Setup/teardownsetUp() & tearDown()init() & deinit
Async supportXCTestExpectationNative async/await
Tag & traitTidak adaBuilt-in .tags, .disabled, .timeLimit
Diff saat gagalHanya nilai akhirEkspresi penuh dengan substitusi nilai
Minimum XcodeSemua versiXcode 16+
UI testingDidukungBelum (gunakan XCTest)

Perlu dicatat bahwa Swift Testing belum mendukung UI testing di iOS 26. Untuk test berbasis XCUIApplication yang menjalankan UI flow di simulator, Anda masih harus pakai XCTest. Karena itu, banyak project enterprise menjalankan kedua framework: Swift Testing untuk unit test logic, dan XCTest untuk UI test end-to-end.

Kesalahan umum saat migrasi dan cara menghindarinya

Dari pengalaman memigrasikan beberapa proyek production, berikut jebakan yang paling sering saya temui:

1. Lupa @testable import

Tanpa @testable import MyApp, Anda tidak bisa akses internal types. Ini sama seperti di XCTest, tapi karena Swift Testing memungkinkan test berupa fungsi global, banyak developer baru lupa menambahkannya saat membuat file test pertama.

2. Menggunakan setUp() di struct @Suite

Struct tidak punya override mechanism, jadi pola override func setUp() tidak berlaku. Gunakan init() dan init() throws untuk setup yang bisa gagal.

3. Salah pakai #expect untuk fatal assertion

Kalau test harus berhenti karena prasyarat gagal (misalnya Optional nil), pakai try #require, bukan #expect. #expect hanya mencatat kegagalan tanpa menghentikan eksekusi, sehingga baris berikutnya bisa crash. Saya pernah menghabiskan satu sore mengejar "test crash random" yang ternyata cuma karena salah pilih macro di sini.

4. Menjalankan test concurrent yang mengakses shared state

Swift Testing menjalankan test secara paralel by default. Kalau test Anda menulis ke file system atau database shared tanpa isolasi, hasilnya bisa flaky. Gunakan .serialized trait di level suite untuk memaksa eksekusi berurutan, atau pisahkan dependency dengan dependency injection.

Untuk pola test yang lebih canggih seperti mocking AI service, lihat panduan kami tentang Foundation Models Framework di iOS 26 yang membahas strategi testing untuk inference on-device.

Pertanyaan yang Sering Diajukan

Apakah Swift Testing akan menggantikan XCTest sepenuhnya?

Dalam jangka pendek tidak. Apple masih merekomendasikan XCTest untuk UI testing dan performance testing karena Swift Testing belum mendukung kedua use case tersebut di iOS 26. Untuk unit test logic, Swift Testing adalah pilihan default ke depan.

Versi Xcode dan Swift minimum untuk Swift Testing?

Swift Testing membutuhkan Xcode 16.0+ dan Swift 6.0+. Toolchain ini sudah tersedia sejak Xcode 16 (akhir 2024) dan menjadi default di Xcode 17 yang dipaketkan dengan iOS 26 SDK.

Bisakah Swift Testing berjalan di Linux atau Windows?

Bisa. Swift Testing adalah package open-source di repository swiftlang/swift-testing yang berjalan di mana saja Swift toolchain didukung, termasuk Linux dan Windows. Ini membuatnya cocok untuk shared library cross-platform dan server-side Swift.

Bagaimana cara menjalankan hanya test dengan tag tertentu di CI?

Gunakan flag xcodebuild test ... -only-testing:TargetName/SuiteName untuk filter berdasarkan suite, atau filter berdasarkan tag dengan opsi --filter di swift test CLI. Di Xcode UI, klik tombol filter di Test Navigator dan pilih tag yang diinginkan.

Kenapa test saya flaky setelah migrasi ke Swift Testing?

Swift Testing menjalankan test secara paralel by default, berbeda dari XCTest yang serial per kelas. Kalau test mengakses shared state (file, database, singleton), gunakan trait .serialized di level @Suite atau refactor menggunakan dependency injection.

Editorial Team
Tentang Penulis Editorial Team

Our team of expert writers and editors.