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
}