Section 13/171 menit
13. Testing Actor dan Sendable
13. Testing Actor dan Sendable
Testing kode konkuren susah karena non-determinisme. Berikut pattern yang terbukti.
Pola 1: Sequential Test untuk Single-Actor
Untuk actor tunggal tanpa interaksi cross-actor, test biasa cukup:
swift
@Test
func incrementWorks() async {
let c = Counter()
await c.increment()
await c.increment()
let value = await c.value
#expect(value == 2)
}
Pola 2: TaskGroup untuk Stress Test
Untuk mengetes safety di bawah concurrent access:
swift
@Test
func concurrentIncrementSafe() async {
let c = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask { await c.increment() }
}
}
let v = await c.value
#expect(v == 1000) // tidak ada race → exact
}
Pola 3: Mock Clock untuk Deterministic Timing
Kalau actor pakai Task.sleep, gunakan abstraction clock:
swift
protocol Clock: Sendable {
func sleep(nanoseconds: UInt64) async throws
}
struct SystemClock: Clock {
func sleep(nanoseconds n: UInt64) async throws {
try await Task.sleep(nanoseconds: n)
}
}
final class MockClock: Clock, @unchecked Sendable {
private let lock = NSLock()
private var waiters: [CheckedContinuation<Void, Error>] = []
func sleep(nanoseconds: UInt64) async throws {
try await withCheckedThrowingContinuation { cont in
lock.withLock { waiters.append(cont) }
}
}
func tickAll() {
let snapshot: [CheckedContinuation<Void, Error>] = lock.withLock {
defer { waiters.removeAll() }
return waiters
}
for w in snapshot { w.resume() }
}
}
Test:
swift
@Test
func debounceFiresAfterDelay() async {
let clock = MockClock()
let debouncer = Debouncer(clock: clock)
Task { await debouncer.trigger() }
await Task.yield() // beri kesempatan trigger jalan
clock.tickAll() // lewati semua sleep
let count = await debouncer.fireCount
#expect(count == 1)
}
Pola 4: Spy via Actor
Test perlu mengamati interaksi tanpa race? Buat actor spy:
swift
actor CallSpy {
private(set) var calls: [String] = []
func record(_ name: String) { calls.append(name) }
}
@Test
func loggerCallsCorrectly() async {
let spy = CallSpy()
let sut = Service(onLog: { msg in
Task { await spy.record(msg) }
})
await sut.run()
// beri waktu detached task selesai
try? await Task.sleep(nanoseconds: 50_000_000)
let calls = await spy.calls
#expect(calls.contains("started"))
}
Pola 5: Sendable Conformance Test
Tes compile-time untuk memastikan tipe tetap Sendable:
swift
// Hanya compile-time check, tidak run
private let _sendableCheck: any Sendable = User.self // hanya jalan kalau User: Sendable
// Atau di test target:
@Test
func userIsSendable() {
func acceptSendable<T: Sendable>(_: T.Type) { }
acceptSendable(User.self) // gagal compile kalau User bukan Sendable
}