Section 10/121 menit

10. Real Use Cases

10. Real Use Cases

Use Case 1: Testing Validation Layer

swift
import Testing

@Suite("User Registration Validation")
struct RegistrationValidationTests {
    let validator = RegistrationValidator()
    
    // Grup: valid inputs
    @Suite("Valid Inputs")
    struct ValidInputs {
        let validator = RegistrationValidator()
        
        @Test("Username valid", arguments: [
            "ari_supriatna",
            "budi123",
            "user-name",
            "a",           // minimum 1 karakter
            String(repeating: "a", count: 30)  // maximum
        ])
        func validUsernames(username: String) throws {
            #expect(throws: Never.self) {
                try validator.validateUsername(username)
            }
        }
    }
    
    // Grup: invalid inputs dengan pesan error spesifik
    @Suite("Invalid Inputs")
    struct InvalidInputs {
        let validator = RegistrationValidator()
        
        @Test("Username terlalu panjang")
        func usernameTooLong() throws {
            let longUsername = String(repeating: "a", count: 31)
            #expect(throws: ValidationError.tooLong) {
                try validator.validateUsername(longUsername)
            }
        }
        
        @Test("Karakter tidak valid dalam username", arguments: [
            "user name",   // spasi
            "user@name",   // @
            "user.name!",  // !
        ])
        func usernameInvalidCharacters(username: String) {
            #expect(throws: ValidationError.invalidCharacters) {
                try validator.validateUsername(username)
            }
        }
    }
    
    // Test integrasi: seluruh form
    @Test("Form valid diterima")
    func validFormAccepted() throws {
        let form = RegistrationForm(
            username: "ari123",
            email: "ari@example.com",
            password: "SecurePass123!"
        )
        
        try validator.validate(form)  // tidak throws = sukses
    }
    
    @Test("Form invalid ditolak dengan semua errors", .tags(.regression))
    func invalidFormReturnsAllErrors() {
        let form = RegistrationForm(username: "", email: "invalid", password: "123")
        let errors = validator.collectErrors(form)
        
        #expect(errors.contains { $0 == .requiredField("username") })
        #expect(errors.contains { $0 == .invalidFormat("email") })
        #expect(errors.contains { $0 == .tooShort(field: "password", minimum: 8, actual: 3) })
        #expect(errors.count == 3)
    }
}

Use Case 2: Testing Networking Layer

swift
import Testing

// Mock untuk testing
actor MockHTTPClient {
    var responses: [URL: Result<Data, Error>] = [:]
    var requestLog: [(url: URL, method: String)] = []
    
    func stub(url: URL, response: Result<Data, Error>) {
        responses[url] = response
    }
    
    func request(url: URL, method: String) async throws -> Data {
        requestLog.append((url: url, method: method))
        switch responses[url] {
        case .success(let data): return data
        case .failure(let error): throw error
        case .none: throw URLError(.badURL)
        }
    }
}

@Suite("User API Client", .serialized)
struct UserAPIClientTests {
    let mockClient: MockHTTPClient
    let apiClient: UserAPIClient
    
    init() async {
        mockClient = MockHTTPClient()
        apiClient = UserAPIClient(httpClient: mockClient)
    }
    
    @Test("Fetch user sukses")
    func fetchUserSuccess() async throws {
        let userData = """
        {"id": "123", "name": "Ari Supriatna", "email": "ari@example.com"}
        """.data(using: .utf8)!
        
        await mockClient.stub(
            url: URL(string: "https://api.example.com/users/123")!,
            response: .success(userData)
        )
        
        let user = try await apiClient.fetchUser(id: "123")
        
        #expect(user.id == "123")
        #expect(user.name == "Ari Supriatna")
        #expect(user.email == "ari@example.com")
    }
    
    @Test("Fetch user not found returns error")
    func fetchUserNotFound() async throws {
        await mockClient.stub(
            url: URL(string: "https://api.example.com/users/999")!,
            response: .failure(URLError(.fileDoesNotExist))
        )
        
        await #expect(throws: UserAPIError.notFound) {
            try await apiClient.fetchUser(id: "999")
        }
    }
    
    @Test("Retry on server error", .timeLimit(.seconds(10)))
    func retryOnServerError() async throws {
        var callCount = 0
        
        // Pertama dua kali gagal, ketiga sukses
        await mockClient.stub(
            url: URL(string: "https://api.example.com/users/123")!,
            response: .failure(URLError(.timedOut))
        )
        
        // Test retry behavior
        await #expect(throws: Never.self) {
            try await apiClient.fetchUserWithRetry(id: "123", maxRetries: 3)
        }
    }
    
    @Test("Concurrent requests tidak interferensi", .tags(.critical))
    func concurrentRequestsIndependent() async throws {
        let userIDs = ["1", "2", "3", "4", "5"]
        
        for id in userIDs {
            let data = """{"id": "\(id)", "name": "User \(id)", "email": "\(id)@test.com"}"""
                .data(using: .utf8)!
            await mockClient.stub(
                url: URL(string: "https://api.example.com/users/\(id)")!,
                response: .success(data)
            )
        }
        
        // Fetch semua secara concurrent
        let users = try await withThrowingTaskGroup(of: User.self) { group in
            for id in userIDs {
                group.addTask { try await self.apiClient.fetchUser(id: id) }
            }
            return try await group.reduce(into: []) { $0.append($1) }
        }
        
        #expect(users.count == userIDs.count)
        #expect(Set(users.map { $0.id }) == Set(userIDs))
    }
}

Use Case 3: Testing Actor dan Concurrency

swift
import Testing

@Suite("Cache Actor")
struct CacheActorTests {
    
    @Test("Set dan get value")
    func setAndGetValue() async {
        let cache = Cache<String, Int>(ttl: .seconds(60))
        
        await cache.set(42, for: "answer")
        let retrieved = await cache.get(for: "answer")
        
        #expect(retrieved == 42)
    }
    
    @Test("Cache miss returns nil")
    func cacheMissReturnsNil() async {
        let cache = Cache<String, Int>(ttl: .seconds(60))
        let value = await cache.get(for: "nonexistent")
        #expect(value == nil)
    }
    
    @Test("TTL expiry — entry kadaluarsa")
    func ttlExpiry() async throws {
        let cache = Cache<String, Int>(ttl: .milliseconds(100))
        
        await cache.set(42, for: "key")
        try await Task.sleep(for: .milliseconds(150))
        
        let value = await cache.get(for: "key")
        #expect(value == nil, "Entry harus sudah kadaluarsa")
    }
    
    @Test("Concurrent writes tidak corrupt state", .tags(.critical))
    func concurrentWritesSafe() async {
        let cache = Cache<String, Int>(maxSize: 1000, ttl: .seconds(60))
        
        await withTaskGroup(of: Void.self) { group in
            for i in 0..<100 {
                group.addTask {
                    await cache.set(i, for: "key_\(i)")
                }
            }
        }
        
        // Verifikasi semua values tersimpan dengan benar
        var successCount = 0
        for i in 0..<100 {
            if await cache.get(for: "key_\(i)") == i {
                successCount += 1
            }
        }
        
        #expect(successCount == 100)
    }
    
    // Parameterized test untuk TTL values
    @Test("Max size enforcement", arguments: [10, 50, 100, 500])
    func maxSizeEnforcement(maxSize: Int) async {
        let cache = Cache<Int, Int>(maxSize: maxSize, ttl: .seconds(60))
        
        for i in 0..<(maxSize + 50) {
            await cache.set(i, for: i)
        }
        
        // Cache tidak boleh melebihi maxSize
        let count = await cache.count
        #expect(count <= maxSize)
    }
}