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)
    }
}