Section 11/161 menit
11. Testing Data Race Safety
11. Testing Data Race Safety
Testing Actor dengan Swift Testing
swift
import Testing
@testable import MyApp
// Test actor behavior — penggunaan langsung tanpa special setup
struct BankAccountTests {
@Test func concurrentTransfers() async throws {
let alice = BankAccount(initialBalance: 1000)
let bob = BankAccount(initialBalance: 500)
// Concurrent transfers — actor serializes access
async let transfer1: Void = alice.transfer(amount: 200, to: bob)
async let transfer2: Void = alice.transfer(amount: 300, to: bob)
// Kedua transfer harus selesai tanpa error atau crash
try await transfer1
try await transfer2
// Total harus konsisten (tidak ada uang yang hilang atau duplikat)
let aliceBalance = await alice.currentBalance
let bobBalance = await bob.currentBalance
#expect(aliceBalance + bobBalance == 1500) // total tidak berubah
}
@Test func highConcurrencyStressTest() async throws {
let account = BankAccount(initialBalance: 10_000)
// 100 concurrent deposits of 10 each
await withTaskGroup(of: Void.self) { group in
for _ in 0..<100 {
group.addTask {
await account.receive(amount: 10)
}
}
}
let finalBalance = await account.currentBalance
#expect(finalBalance == 11_000) // 10_000 + (100 * 10)
}
}
Mocking Actor Dependencies
swift
// Protocol untuk testability
protocol DataStoreProtocol: Actor {
func save(_ data: Data, key: String) async throws
func load(key: String) async throws -> Data
}
// Production implementation
actor FileDataStore: DataStoreProtocol {
private let directory: URL
init(directory: URL) {
self.directory = directory
}
func save(_ data: Data, key: String) async throws {
let url = directory.appendingPathComponent(key)
try data.write(to: url)
}
func load(key: String) async throws -> Data {
let url = directory.appendingPathComponent(key)
return try Data(contentsOf: url)
}
}
// Test mock — juga actor untuk protocol conformance
actor MockDataStore: DataStoreProtocol {
var savedData: [String: Data] = [:]
var saveCallCount = 0
var loadCallCount = 0
func save(_ data: Data, key: String) async throws {
saveCallCount += 1
savedData[key] = data
}
func load(key: String) async throws -> Data {
loadCallCount += 1
guard let data = savedData[key] else {
throw DataError.notFound
}
return data
}
}
// Service yang menggunakan protocol
actor CachingService<Store: DataStoreProtocol> {
private let store: Store
private var memoryCache: [String: Data] = [:]
init(store: Store) {
self.store = store
}
func get(key: String) async throws -> Data {
if let cached = memoryCache[key] { return cached }
let data = try await store.load(key: key)
memoryCache[key] = data
return data
}
}
// Test
struct CachingServiceTests {
@Test func cachesAfterFirstLoad() async throws {
let mockStore = MockDataStore()
await mockStore.save(Data([1, 2, 3]), key: "test")
let service = CachingService(store: mockStore)
// Pertama kali: hit store
let result1 = try await service.get(key: "test")
let callCount1 = await mockStore.loadCallCount
#expect(callCount1 == 1)
// Kedua kali: hit memory cache
let result2 = try await service.get(key: "test")
let callCount2 = await mockStore.loadCallCount
#expect(callCount2 == 1) // tidak bertambah
#expect(result1 == result2)
}
}