Swift Testing Framework Wawancara 2026: Makro #expect dan #require vs XCTest
Kuasai Swift Testing Framework baru untuk wawancara iOS: makro #expect dan #require, migrasi dari XCTest, pola lanjutan, dan jebakan umum.

Diperkenalkan pada WWDC 2024 dan dirilis bersama Swift 6 dan Xcode 16, Swift Testing merupakan reimajinasi penuh terhadap cara kerja pengujian di Swift. Framework ini menggantikan lebih dari 40 assertion XCTest dengan hanya dua makro: #expect dan #require. Pewawancara kini secara rutin menguji pengetahuan ini selama wawancara teknis iOS.
Setiap bagian mencerminkan pertanyaan wawancara teknis dengan jawaban rinci dan kode yang berfungsi. Progresinya bergerak dari konsep dasar menuju pola lanjutan.
Dasar-Dasar Swift Testing
Pertanyaan 1: Apa perbedaan utama antara Swift Testing dan XCTest?
Swift Testing membawa lima perubahan mendasar dibandingkan XCTest:
- Sintaks deklaratif: atribut
@Testalih-alih awalantest - Dua makro universal:
#expectdan#requiremenggantikan lebih dari 40 assertion - Paralel secara default: semua pengujian berjalan bersamaan
- Dukungan async native: integrasi penuh dengan Swift Concurrency
- Lintas platform: berjalan di platform Apple, Linux, dan Windows
import XCTest
import Testing
// ❌ Legacy XCTest pattern
class UserServiceXCTests: XCTestCase {
// Must start with "test"
func testUserCreation() {
let user = User(name: "Alice", age: 25)
// Multiple verbose assertions
XCTAssertNotNil(user)
XCTAssertEqual(user.name, "Alice")
XCTAssertGreaterThan(user.age, 18)
XCTAssertTrue(user.isValid)
}
}
// ✅ Modern Swift Testing pattern
@Test("User creation with valid data")
func userCreation() {
let user = User(name: "Alice", age: 25)
// Single macro for all verifications
#expect(user.name == "Alice")
#expect(user.age > 18)
#expect(user.isValid)
}Perbedaan utama terletak pada ekspresivitas: Swift Testing menggunakan ekspresi Swift standar alih-alih assertion khusus, sehingga pengujian lebih mudah dibaca dan pesan error lebih informatif.
Pertanyaan 2: Bagaimana cara kerja makro #expect?
Makro #expect memvalidasi bahwa ekspresi boolean bernilai benar. Makro ini secara otomatis menangkap nilai yang dievaluasi untuk memberikan pesan error yang detail. Berbeda dengan XCTAssert, ia menggunakan sintaks Swift native.
import Testing
@Test func basicExpectations() {
let numbers = [1, 2, 3, 4, 5]
let user = User(name: "Bob", email: "bob@example.com")
// Simple comparisons - standard Swift expression
#expect(numbers.count == 5)
#expect(user.name == "Bob")
// Comparisons with operators
#expect(numbers.first! < numbers.last!)
#expect(user.email.contains("@"))
// Nil checking
#expect(numbers.first != nil)
// Collection verification
#expect(numbers.contains(3))
#expect(!numbers.isEmpty)
}
@Test func expectWithCustomMessage() {
let balance = 150.0
let withdrawAmount = 200.0
// Custom message to clarify intent
#expect(
balance >= withdrawAmount,
"Insufficient funds: balance \(balance) < withdrawal \(withdrawAmount)"
)
}Ketika #expect gagal, pengujian tetap berjalan. Karakteristik ini memungkinkan pengumpulan banyak kegagalan dalam satu eksekusi, sehingga memudahkan diagnosis.
Pertanyaan 3: Apa perbedaan antara #expect dan #require?
Perbedaan mendasar terletak pada perilaku setelah kegagalan:
#expect: mencatat kegagalan dan melanjutkan eksekusi#require: mencatat kegagalan dan segera menghentikan pengujian
#require harus selalu digunakan dengan try karena dapat melempar error.
import Testing
struct ApiResponse {
let data: Data?
let items: [Item]?
}
@Test func demonstrateExpectContinues() {
let values = [1, 2, 3]
// First #expect fails but test continues
#expect(values.count == 10) // ❌ Failure recorded
// These verifications still execute
#expect(values.first == 1) // ✅ Success
#expect(values.last == 3) // ✅ Success
// Result: 1 failure, 2 successes in the same test
}
@Test func demonstrateRequireStops() throws {
let response = ApiResponse(data: nil, items: nil)
// #require stops immediately if condition fails
let data = try #require(response.data) // ❌ Failure and STOP
// This code NEVER executes if data is nil
let json = try JSONDecoder().decode(User.self, from: data)
#expect(json.name == "Alice")
}Dalam praktiknya, #require secara sempurna menggantikan XCTUnwrap untuk unwrap optional yang aman.
Gunakan #require ketika langkah berikutnya bergantung pada hasil (unwrap, prasyarat). Gunakan #expect untuk verifikasi independen yang dapat gagal tanpa memblokir sisa pengujian.
Pola Lanjutan dengan #require
Pertanyaan 4: Bagaimana menggunakan #require untuk unwrap optional?
#require unggul dalam unwrap optional. Ia mengembalikan nilai non-optional jika ada, atau langsung menggagalkan pengujian jika nil.
import Testing
struct UserProfile {
let id: Int
let name: String
let settings: Settings?
}
struct Settings {
let theme: String
let notifications: Bool
}
@Test func unwrapOptionalChain() throws {
let profile = UserProfile(
id: 1,
name: "Alice",
settings: Settings(theme: "dark", notifications: true)
)
// Safe unwrap - stops if nil
let settings = try #require(profile.settings)
// Now settings is no longer optional
#expect(settings.theme == "dark")
#expect(settings.notifications == true)
}
@Test func unwrapArrayElement() throws {
let users = ["Alice", "Bob", "Charlie"]
// Unwrap first element
let firstUser = try #require(users.first)
#expect(firstUser == "Alice")
// Unwrap with safe index
let secondUser = try #require(users[safe: 1])
#expect(secondUser == "Bob")
}
@Test func unwrapDictionaryValue() throws {
let config: [String: Any] = [
"apiUrl": "https://api.example.com",
"timeout": 30,
"retryCount": 3
]
// Unwrap and cast in a single operation
let apiUrl = try #require(config["apiUrl"] as? String)
let timeout = try #require(config["timeout"] as? Int)
#expect(apiUrl.contains("https"))
#expect(timeout > 0)
}Pendekatan ini menghapus piramida guard let dan membuat kode pengujian linier serta mudah dibaca.
Pertanyaan 5: Bagaimana menguji bahwa fungsi melempar error?
Swift Testing menyediakan #expect(throws:) untuk memverifikasi bahwa fungsi melempar error tertentu.
import Testing
enum ValidationError: Error, Equatable {
case emptyName
case invalidEmail
case underAge(minimum: Int)
}
struct Validator {
static func validateUser(name: String, email: String, age: Int) throws {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
guard email.contains("@") else {
throw ValidationError.invalidEmail
}
guard age >= 18 else {
throw ValidationError.underAge(minimum: 18)
}
}
}
@Test func testThrowsSpecificError() {
// Verify a specific error is thrown
#expect(throws: ValidationError.emptyName) {
try Validator.validateUser(name: "", email: "test@mail.com", age: 25)
}
#expect(throws: ValidationError.invalidEmail) {
try Validator.validateUser(name: "Alice", email: "invalid", age: 25)
}
}
@Test func testThrowsErrorType() {
// Verify error type without specific value
#expect(throws: ValidationError.self) {
try Validator.validateUser(name: "Bob", email: "bob@mail.com", age: 15)
}
}
@Test func testThrowsWithInspection() throws {
// Capture error for detailed inspection
let error = try #require(
throws: ValidationError.self
) {
try Validator.validateUser(name: "Charlie", email: "charlie@mail.com", age: 16)
}
// Verify error details
if case .underAge(let minimum) = error {
#expect(minimum == 18)
}
}Sintaks ini menggantikan XCTAssertThrowsError dengan API yang lebih jelas dan type-safe.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Pengorganisasian Pengujian dengan @Test dan @Suite
Pertanyaan 6: Bagaimana mengorganisasi pengujian dengan @Suite?
@Suite mengelompokkan pengujian terkait secara logis. Berbeda dengan XCTestCase, ia tidak memerlukan pewarisan kelas.
import Testing
// Suite for authentication tests
@Suite("Authentication Tests")
struct AuthenticationTests {
// Shared property for all tests in the suite
let authService = AuthService()
@Test("Login with valid credentials succeeds")
func loginWithValidCredentials() async throws {
let result = try await authService.login(
email: "user@example.com",
password: "validPass123"
)
#expect(result.isSuccess)
let token = try #require(result.token)
#expect(!token.isEmpty)
}
@Test("Login with invalid password fails")
func loginWithInvalidPassword() async {
let result = await authService.login(
email: "user@example.com",
password: "wrong"
)
#expect(!result.isSuccess)
#expect(result.error == .invalidCredentials)
}
}
// Nested suites for hierarchical organization
@Suite("User Management")
struct UserManagementTests {
@Suite("Creation")
struct CreationTests {
@Test func createUserWithValidData() {
// Creation test
}
@Test func createUserWithDuplicateEmail() {
// Duplicate error test
}
}
@Suite("Deletion")
struct DeletionTests {
@Test func deleteExistingUser() {
// Deletion test
}
@Test func deleteNonExistentUser() {
// Error test
}
}
}Suites memungkinkan menjalankan subset pengujian dan mengorganisasi laporan secara mudah dibaca.
Pertanyaan 7: Bagaimana menggunakan trait untuk mengonfigurasi pengujian?
Trait memodifikasi perilaku pengujian: kondisi eksekusi, tag, timeout, dan sebagainya.
import Testing
@Suite("API Integration Tests")
struct APITests {
// Temporarily disabled test
@Test(.disabled("Backend under maintenance"))
func fetchUserProfile() async {
// Does not execute
}
// Conditional test based on platform
@Test
@available(iOS 17, *)
func useNewAPIFeature() {
// Executes only on iOS 17+
}
// Test with tags for filtering
@Test(.tags(.critical, .network))
func criticalNetworkOperation() async throws {
// Tagged test for filtering
}
// Test with custom timeout
@Test(.timeLimit(.minutes(2)))
func longRunningOperation() async {
// Must complete within 2 minutes
}
// Trait combination
@Test(
"Complex data sync",
.tags(.slow),
.timeLimit(.minutes(5)),
.bug("JIRA-1234", "Flaky on CI")
)
func complexDataSync() async throws {
// Test documented with known bug
}
}
// Custom tag definitions
extension Tag {
@Tag static var critical: Self
@Tag static var network: Self
@Tag static var slow: Self
}Trait membuat pengujian mendokumentasikan dirinya sendiri dan memungkinkan eksekusi selektif via baris perintah.
Pengujian Berparameter
Pertanyaan 8: Bagaimana membuat pengujian berparameter?
Swift Testing memungkinkan menjalankan pengujian yang sama dengan input berbeda melalui parameter.
import Testing
struct EmailValidator {
static func isValid(_ email: String) -> Bool {
let pattern = #"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"#
return email.range(of: pattern, options: .regularExpression) != nil
}
}
// Parameterized test with a collection
@Test(arguments: [
"user@example.com",
"test.name@domain.org",
"contact@company.co.uk"
])
func validEmailsAreAccepted(email: String) {
#expect(EmailValidator.isValid(email))
}
@Test(arguments: [
"invalid",
"@nodomain.com",
"no@tld",
"spaces in@email.com"
])
func invalidEmailsAreRejected(email: String) {
#expect(!EmailValidator.isValid(email))
}
// Test with tuples for input/output cases
@Test(arguments: [
("hello", "HELLO"),
("World", "WORLD"),
("Swift", "SWIFT")
])
func uppercaseConversion(input: String, expected: String) {
#expect(input.uppercased() == expected)
}
// Test with Cartesian product of two collections
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinationTest(number: Int, letter: String) {
// Runs for (1,"a"), (1,"b"), (2,"a"), (2,"b"), (3,"a"), (3,"b")
let combined = "\(number)\(letter)"
#expect(combined.count == 2)
}Setiap kombinasi parameter menghasilkan pengujian independen, sehingga memudahkan identifikasi kasus yang gagal.
Pertanyaan 9: Bagaimana menguji kode asinkron?
Swift Testing mengintegrasikan async/await secara native, sehingga sangat menyederhanakan pengujian asinkron.
import Testing
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
func clear() {
items.removeAll()
}
}
@Suite("Async Operations")
struct AsyncTests {
@Test func basicAsyncOperation() async {
// No need for expectation or wait
let result = await fetchData()
#expect(!result.isEmpty)
}
@Test func asyncWithTimeout() async throws {
// Use Task.sleep to simulate delay
try await Task.sleep(for: .milliseconds(100))
let data = await loadConfiguration()
let config = try #require(data)
#expect(config.isValid)
}
@Test func testActorIsolation() async {
let store = DataStore()
// Operations on actor sequentially
await store.add("Item 1")
await store.add("Item 2")
let items = await store.getAll()
#expect(items.count == 2)
#expect(items.contains("Item 1"))
}
@Test func testConcurrentOperations() async {
let store = DataStore()
// Concurrent execution with TaskGroup
await withTaskGroup(of: Void.self) { group in
for i in 1...10 {
group.addTask {
await store.add("Item \(i)")
}
}
}
let items = await store.getAll()
#expect(items.count == 10)
}
}
// Async helpers for tests
func fetchData() async -> [String] {
try? await Task.sleep(for: .milliseconds(50))
return ["data1", "data2"]
}
func loadConfiguration() async -> Configuration? {
Configuration(isValid: true)
}
struct Configuration {
let isValid: Bool
}Swift Testing menjalankan pengujian secara paralel secara default. Untuk pengujian yang memodifikasi state bersama, gunakan .serialized pada suite atau isolasi state dengan actor.
Migrasi dari XCTest ke Swift Testing
Pertanyaan 10: Bagaimana bermigrasi secara bertahap dari XCTest?
Kedua framework dapat hidup bersama dalam proyek yang sama. Migrasi bertahap direkomendasikan.
import XCTest
import Testing
// ⚠️ CRITICAL RULE: Never mix frameworks in the same test
// ❌ INCORRECT - Mixing forbidden
class BadMixedTest: XCTestCase {
func testMixed() {
#expect(true) // Does not work in XCTestCase
}
}
// ✅ CORRECT - Pure XCTest for existing tests
class LegacyUserTests: XCTestCase {
func testUserCreation() {
let user = User(name: "Test")
XCTAssertNotNil(user)
XCTAssertEqual(user.name, "Test")
}
}
// ✅ CORRECT - Swift Testing for new tests
@Suite("User Tests - Modern")
struct ModernUserTests {
@Test func userCreation() {
let user = User(name: "Test")
#expect(user.name == "Test")
}
}
// Migration strategy by file
// 1. Identify tests to migrate (start with simplest)
// 2. Create new file with @Suite
// 3. Rewrite tests one by one
// 4. Delete old XCTest file once validated| XCTest | Swift Testing |
|--------|---------------|
| XCTAssertTrue(x) | #expect(x) |
| XCTAssertFalse(x) | #expect(!x) |
| XCTAssertEqual(a, b) | #expect(a == b) |
| XCTAssertNil(x) | #expect(x == nil) |
| XCTAssertNotNil(x) | #expect(x != nil) |
| XCTUnwrap(x) | try #require(x) |
| XCTAssertThrowsError | #expect(throws:) |
Pertanyaan 11: Fitur XCTest apa yang belum ada di Swift Testing?
Swift Testing (Swift 6) belum mencakup semua use case XCTest.
// ❌ NOT SUPPORTED: Performance tests
// Stick with XCTest for measuring performance
class PerformanceTests: XCTestCase {
func testPerformance() {
measure {
// Code to measure
_ = (0..<1000).map { $0 * 2 }
}
}
}
// ❌ NOT SUPPORTED: UI tests (XCUITest)
// Continue using XCUITest for interface tests
class UITests: XCTestCase {
func testLoginFlow() {
let app = XCUIApplication()
app.launch()
// UI tests...
}
}
// ✅ SUPPORTED: Async integration tests
@Test func integrationTest() async throws {
let api = APIClient()
let response = try await api.fetchUsers()
#expect(!response.isEmpty)
}
// ✅ SUPPORTED: Mocking with protocols
@Test func mockingWithProtocols() async {
let mockService = MockUserService()
let viewModel = UserViewModel(service: mockService)
await viewModel.loadUser(id: 1)
#expect(viewModel.user?.name == "Mock User")
}Untuk proyek dengan pengujian UI atau performa, tetap gunakan XCTest untuk kasus-kasus spesifik tersebut.
Siap menguasai wawancara iOS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Pertanyaan Wawancara yang Menjebak
Pertanyaan 12: Mengapa #require memerlukan try tetapi #expect tidak?
#require dapat melempar error ketika kondisi gagal karena harus menghentikan pengujian. #expect hanya mencatat kegagalan dan melanjutkan, jadi tidak melempar apa-apa.
import Testing
@Test func explainTryRequirement() throws {
let optionalValue: String? = nil
// #expect returns Void - no error thrown
// Test continues even if it fails
#expect(optionalValue != nil) // No try
// #require can throw ExpectationFailedError
// Test stops if it fails
// Must be marked with try
let value = try #require(optionalValue) // Requires try
// If we reach here, optionalValue was not nil
#expect(!value.isEmpty)
}
// The internal signature resembles:
// func #expect(_ condition: Bool) -> Void
// func #require<T>(_ value: T?) throws -> TPemisahan arsitektural ini memungkinkan kompiler menjamin penanganan error yang benar.
Pertanyaan 13: Bagaimana Swift Testing mengelola paralelisme?
Secara default, semua pengujian berjalan paralel, sehingga mempercepat suite tetapi memerlukan isolasi state yang benar.
import Testing
// ❌ PROBLEM: Mutable shared state
var sharedCounter = 0 // Dangerous in parallel!
@Suite("Problematic Parallel Tests")
struct ProblematicTests {
@Test func incrementCounter1() {
sharedCounter += 1 // Race condition!
}
@Test func incrementCounter2() {
sharedCounter += 1 // Race condition!
}
}
// ✅ SOLUTION 1: Force sequential execution
@Suite("Sequential Tests", .serialized)
struct SequentialTests {
static var counter = 0
@Test func first() {
Self.counter += 1
#expect(Self.counter == 1)
}
@Test func second() {
Self.counter += 1
#expect(Self.counter == 2)
}
}
// ✅ SOLUTION 2: Isolate state per test
@Suite("Isolated Tests")
struct IsolatedTests {
@Test func independentTest1() {
var localCounter = 0
localCounter += 1
#expect(localCounter == 1)
}
@Test func independentTest2() {
var localCounter = 0
localCounter += 1
#expect(localCounter == 1)
}
}
// ✅ SOLUTION 3: Use actor for shared state
actor TestState {
var value = 0
func increment() -> Int {
value += 1
return value
}
}
@Suite("Actor-based Tests")
struct ActorTests {
let state = TestState()
@Test func safeIncrement() async {
let result = await state.increment()
#expect(result > 0)
}
}Trait .serialized menjamin eksekusi sekuensial pada seluruh suite.
Pertanyaan 14: Bagaimana menggunakan confirmation untuk callback?
Untuk API dengan callback (non-async), Swift Testing menyediakan confirmation.
import Testing
// Legacy service with callback
class LegacyService {
func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
completion(.success("Data loaded"))
}
}
}
@Test func testCallbackWithConfirmation() async {
let service = LegacyService()
// confirmation waits for it to be called
await confirmation("Data callback received") { confirm in
service.fetchData { result in
if case .success(let data) = result {
#expect(data == "Data loaded")
confirm() // Signals callback was executed
}
}
}
}
// For callbacks called multiple times
@Test func testMultipleCallbacks() async {
let publisher = EventPublisher()
// expectedCount specifies expected call count
await confirmation("Events received", expectedCount: 3) { confirm in
publisher.onEvent = { event in
#expect(!event.isEmpty)
confirm() // Called 3 times
}
publisher.emit("Event 1")
publisher.emit("Event 2")
publisher.emit("Event 3")
}
}
class EventPublisher {
var onEvent: ((String) -> Void)?
func emit(_ event: String) {
DispatchQueue.global().async {
self.onEvent?(event)
}
}
}confirmation secara elegan menggantikan XCTestExpectation dan wait(for:timeout:).
Kesimpulan
Swift Testing mewakili masa depan pengujian di platform Apple. Dua makro #expect dan #require sangat menyederhanakan penulisan pengujian sekaligus meningkatkan kualitas pesan error.
Poin penting yang perlu diingat untuk wawancara:
- ✅
#expectmelanjutkan setelah kegagalan,#requiresegera berhenti - ✅
#requirememerlukantrykarena dapat melempar error - ✅ Swift Testing berjalan paralel secara default
- ✅ Kedua framework dapat hidup bersama tetapi tidak boleh dicampur dalam pengujian yang sama
- ✅ XCTest tetap diperlukan untuk pengujian UI dan performa
- ✅ Trait memungkinkan konfigurasi perilaku pengujian secara halus
- ✅ Pengujian berparameter menghindari duplikasi kode
Migrasi ke Swift Testing dapat dilakukan bertahap, file demi file, dimulai dari pengujian paling sederhana.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Wawancara StoreKit 2: Manajemen Langganan dan Validasi Tanda Terima
Kuasai pertanyaan wawancara iOS tentang StoreKit 2, manajemen langganan, validasi tanda terima, dan implementasi pembelian dalam aplikasi dengan contoh kode Swift praktis.

Wawancara iOS Push Notifications 2026: APNs, token, dan troubleshooting
Panduan lengkap untuk mempersiapkan wawancara iOS tentang Push Notifications, APNs, manajemen token, dan troubleshooting. Pertanyaan umum dengan jawaban mendetail.

25 Pertanyaan Interview Swift Teratas untuk Developer iOS
Persiapkan diri untuk interview iOS dengan 25 pertanyaan Swift yang paling sering ditanyakan: optionals, closures, ARC, protocols, async/await, dan pola lanjutan.