Section 12/161 menit
12. Testing Concurrent Code
12. Testing Concurrent Code
Tantangan Testing Async Code
swift
import Testing
// MARK: - Testing async functions
@Suite("Async API Tests")
struct AsyncTests {
@Test("Fetch user berhasil")
func testFetchUser() async throws {
let service = UserService()
let user = try await service.fetchUser(id: "123")
#expect(user.name == "Expected Name")
}
// Test dengan timeout
@Test("Network call selesai dalam 5 detik", .timeLimit(.seconds(5)))
func testNetworkTimeout() async throws {
let service = NetworkService()
let result = try await service.fetchData(from: testURL)
#expect(!result.isEmpty)
}
}
var testURL: URL { URL(string: "https://example.com")! }
struct UserService { func fetchUser(id: String) async throws -> User { User(id: id, name: "Expected Name") } }
struct NetworkService { func fetchData(from url: URL) async throws -> Data { Data() } }
Testing Actor State
swift
// Test yang verifikasi actor isolation
@Suite("Actor Tests")
struct ActorTests {
@Test("Concurrent writes tidak menyebabkan data corruption")
func testConcurrentWrites() async throws {
let counter = SafeCounter()
// Launch 100 concurrent tasks
await withTaskGroup(of: Void.self) { group in
for _ in 0..<100 {
group.addTask {
await counter.increment()
}
}
}
let finalCount = await counter.value
#expect(finalCount == 100, "Seharusnya tepat 100 setelah 100 increments")
}
@Test("Reentrancy tidak menyebabkan double-fetch")
func testNoDoubleFetch() async throws {
let cache = TrackingCache()
// Concurrent access untuk key yang sama
async let r1 = cache.getValue(for: "key1")
async let r2 = cache.getValue(for: "key1")
let (v1, v2) = try await (r1, r2)
#expect(v1 == v2)
let fetchCount = await cache.fetchCount
#expect(fetchCount == 1, "Key yang sama hanya boleh di-fetch sekali")
}
}
actor SafeCounter {
private(set) var value = 0
func increment() { value += 1 }
}
actor TrackingCache {
private var data: [String: String] = [:]
private var inFlight: [String: Task<String, Error>] = [:]
private(set) var fetchCount = 0
func getValue(for key: String) async throws -> String {
if let cached = data[key] { return cached }
if let existing = inFlight[key] { return try await existing.value }
let task = Task { [weak self] () throws -> String in
guard let self else { throw CacheError.deallocated }
// Simulate network fetch
try await Task.sleep(for: .milliseconds(10))
await self.recordFetch(key: key)
return "value-\(key)"
}
inFlight[key] = task
let result = try await task.value
data[key] = result
inFlight.removeValue(forKey: key)
return result
}
private func recordFetch(key: String) {
fetchCount += 1
data[key] = "value-\(key)"
}
}
Testing dengan Fake Clock
swift
// Test time-based logic tanpa menunggu waktu nyata
// Menggunakan TestClock dari Point-Free's swift-clocks package
@Test("Retry exponential backoff menunggu waktu yang benar")
func testRetryBackoff() async throws {
// Dengan TestClock, kita bisa advance waktu manual
// tanpa menunggu detik/menit nyata
var attempts: [Date] = []
// Biasanya pakai: let clock = TestClock()
// Di sini kita mock dengan ContinuousClock untuk simplifikasi
let clock = ContinuousClock()
let maxAttempts = 3
do {
try await retryWithBackoff(
clock: clock,
maxAttempts: maxAttempts,
initialDelay: .milliseconds(10) // kecil untuk test cepat
) {
attempts.append(Date())
throw TestError.alwaysFail
}
} catch {}
#expect(attempts.count == maxAttempts)
}
enum TestError: Error { case alwaysFail }
func retryWithBackoff<C: Clock>(
clock: C,
maxAttempts: Int,
initialDelay: C.Duration,
operation: () async throws -> Void
) async throws where C.Duration == Duration {
var delay = initialDelay
for attempt in 1...maxAttempts {
do {
try await operation()
return
} catch {
guard attempt < maxAttempts else { throw error }
try await Task.sleep(for: delay, clock: clock)
delay = delay * 2
}
}
}